• Stars
    star
    146
  • Rank 251,940 (Top 5 %)
  • Language
    PHP
  • License
    MIT License
  • Created over 6 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

PHPStan rules to detect disallowed method & function calls, constant, namespace, attribute & superglobal usages

Disallowed calls for PHPStan

PHPStan rules to detect disallowed calls and more, without running the code.

PHP Tests

There are some functions, methods, and constants which should not be used in production code. One good example is var_dump(), it is often used to quickly debug problems but should be removed before committing the code. And sometimes it's not.

Another example would be a generic logger. Let's say you're using one of the generic logging libraries but you have your own logger that will add some more info, or sanitize data, before calling the generic logger. Your code should not call the generic logger directly but should instead use your custom logger.

This PHPStan extension will detect such usage, if configured. It should be noted that this extension is not a way to defend against or detect hostile developers, as they can obfuscate the calls for example. This extension is meant to be another pair of eyes, detecting your own mistakes, it doesn't aim to detect-all-the-things.

Tests will provide examples what is currently detected. If it's not covered by tests, it might be, but most probably will not be detected. *Test.php files are the tests, start with those, the analyzed test code is in src, required test classes in libs.

Feel free to file issues or create pull requests if you need to detect more calls.

Installation

Install the extension using Composer:

composer require --dev spaze/phpstan-disallowed-calls

PHPStan, the PHP Static Analysis Tool, is a requirement.

If you use phpstan/extension-installer, you are all set and can skip to configuration.

For manual installation, add this to your phpstan.neon:

includes:
    - vendor/spaze/phpstan-disallowed-calls/extension.neon

Configuration

You can start by including disallowed-dangerous-calls.neon in your phpstan.neon:

includes:
    - vendor/spaze/phpstan-disallowed-calls/disallowed-dangerous-calls.neon

disallowed-dangerous-calls.neon can also serve as a template when you'd like to extend the configuration to disallow some other functions or methods, copy it and modify to your needs. You can also allow a previously disallowed dangerous call in a defined path (see below) in your own config by using the same call or method key.

If you want to disallow program execution functions (exec(), shell_exec() & friends) including the backtick operator (`...`, disallowed when shell_exec() is disallowed), include disallowed-execution-calls.neon:

includes:
    - vendor/spaze/phpstan-disallowed-calls/disallowed-execution-calls.neon

I'd recommend you include both:

includes:
    - vendor/spaze/phpstan-disallowed-calls/disallowed-dangerous-calls.neon
    - vendor/spaze/phpstan-disallowed-calls/disallowed-execution-calls.neon

To disallow some insecure or potentially insecure calls (like md5(), sha1(), mysql_query()), include disallowed-insecure-calls.neon:

includes:
    - vendor/spaze/phpstan-disallowed-calls/disallowed-insecure-calls.neon

Some function calls are better when done for example with some parameters set to a defined value ("strict calls"). For example in_array() better also check for types to prevent some type juggling bugs. Include disallowed-loose-calls.neon to disallow calls without such parameters set ("loose calls").

includes:
    - vendor/spaze/phpstan-disallowed-calls/disallowed-loose-calls.neon

Disallow disabled functions & classes

Run bin/generate-from-disabled.php to generate a configuration based on the disable_functions & disable_classes PHP options. The configuration will be dumped to STDOUT in NEON format, you can save it to a file and include it in your PHPStan configuration.

The file needs to be pre-generated because different environments often have different PHP configurations and if disabled functions & classes would be disallowed dynamically, the configuration would vary when executed for example in dev & CI environments.

Custom rules

There are several different types (and configuration keys) that can be disallowed:

  1. disallowedMethodCalls - for detecting $object->method() calls
  2. disallowedStaticCalls - for static calls Class::method()
  3. disallowedFunctionCalls - for functions like function()
  4. disallowedConstants - for constants like DATE_ISO8601 or DateTime::ISO8601 (which needs to be split to class: DateTime & constant: ISO8601 in the configuration, see notes below)
  5. disallowedNamespaces or disallowedClasses - for usages of classes or classes from a namespace
  6. disallowedSuperglobals - for usages of superglobal variables like $GLOBALS or $_POST
  7. disallowedAttributes - for attributes like #[Entity(class: Foo::class, something: true)]

Use them to add rules to your phpstan.neon config file. I like to use a separate file (disallowed-calls.neon) for these which I'll include later on in the main phpstan.neon config file. Here's an example, update to your needs:

parameters:
    disallowedMethodCalls:
        -
            method: 'PotentiallyDangerous\Logger::log()'  # `function` is an alias of `method`
            message: 'use our own logger instead'
            errorTip: 'see https://our-docs.example/logging on how logging should be used'
        -
            method: 'Redis::connect()'
            message: 'use our own Redis instead'
            errorIdentifier: 'redis.connect'

    disallowedStaticCalls:
        -
            method: 'PotentiallyDangerous\Debugger::log()'
            message: 'use our own logger instead'

    disallowedFunctionCalls:
        -
            function: 'var_dump()'  # `method` is an alias of `function`
            message: 'use logger instead'
        -
            function: 'print_r()'
            message: 'use logger instead'

    disallowedConstants:
        -
            constant: 'DATE_ISO8601'
            message: 'use DATE_ATOM instead'
        -
            class: 'DateTimeInterface'
            constant: 'ISO8601'
            message: 'use DateTimeInterface::ATOM instead'

    disallowedNamespaces:  # `disallowedClasses` is an alias of `disallowedNamespaces`
        -
            class: 'Symfony\Component\HttpFoundation\RequestStack'  # `class` is an alias of `namespace`
            message: 'pass Request via controller instead'
            allowIn:
                - tests/*
        -
            namespace: 'Assert\*'  # `namespace` is an alias of `class`
            message: 'use Webmozart\Assert instead'

    disallowedSuperglobals:
        -
            superglobal: '$_GET'
            message: 'use the Request methods instead'

    disallowedAttributes:
        -
            attribute: Entity
            message: 'use our own custom Entity instead'

The message key is optional. Functions and methods can be specified with or without (). Omitting () is not recommended though to avoid confusing method calls with class constants.

If you want to disallow multiple calls, constants, class constants (same-class only), classes, namespaces or variables that share the same message and other config keys, you can use a list or an array to specify them all:

parameters:
    disallowedFunctionCalls:
        -
            function:
                - 'var_dump()'
                - 'print_r()'
            message: 'use logger instead'

    disallowedConstants:
        -
            class: 'DateTimeInterface'
            constant: ['ISO8601', 'RFC3339', 'W3C']
            message: 'use DateTimeInterface::ATOM instead'

The optional errorTip key can be used to show an additional message prefixed with 💡 that's rendered below the error message in the analysis result.

The errorIdentifier key is optional. It can be used to provide a unique identifier to the PHPStan error.

Use wildcard (*) to ignore all functions, methods, classes, namespaces starting with a prefix, for example:

parameters:
    disallowedFunctionCalls:
        -
            function: 'pcntl_*()'

The wildcard makes most sense when used as the rightmost character of the function or method name, optionally followed by (), but you can use it anywhere for example to disallow all functions that end with y: function: '*y()'. The matching is powered by fnmatch so you can use even multiple wildcards if you wish because w*y n*t.

If there's this one function, method, namespace, attribute (or multiple of them) that you'd like to exclude from the set, you can do that with exclude:

parameters:
    disallowedFunctionCalls:
        -
            function: 'pcntl_*()'
            exclude:
                - 'pcntl_foobar()'

This config would disallow all pcntl functions except (an imaginary) pcntl_foobar(). Please note exclude also accepts fnmatch patterns so please be careful to not create a contradicting config, and that it can accept both a string and an array of strings.

Another option how to limit the set of functions or methods selected by the function or method directive is a file path in which these are defined which mostly makes sense when a fnmatch pattern is used in those directives. Imagine a use case in which you want to disallow any function or method defined in any namespace, or none at all, by this legacy package:

parameters:
    disallowedFunctionCalls:
        -
            function: '*'
            definedIn:
                - 'vendor/foo/bar'
    disallowedMethodCalls:
        -
            method: '*'
            definedIn:
                - 'vendor/foo/bar'
    filesRootDir: %rootDir%/../../..

Relative paths in definedIn are resolved based on the current working directory. When running PHPStan from a directory or subdirectory which is not your "root" directory, the paths will probably not work. Use filesRootDir in that case to specify an absolute root directory, you can use %rootDir% to start with PHPStan's root directory (usually /something/something/vendor/phpstan/phpstan) and then .. from there to your "root" directory. filesRootDir is also used to configure all allowIn directives, see below.

You can treat some language constructs as functions and disallow it in disallowedFunctionCalls. Currently detected language constructs are:

  • die()
  • echo()
  • empty()
  • eval()
  • exit()
  • print()

To disallow naive object creation (new ClassName() or new $classname), disallow NameSpace\ClassName::__construct in disallowedMethodCalls. Works even when there's no constructor defined in that class.

Disallowing constants

Constants are a special breed. First, a constant needs to be disallowed on the declaring class. That means, that instead of disallowing Date::ISO8601 or DateTimeImmutable::ISO8601, you need to disallow DateTimeInterface::ISO8601. The reason for this is that one might expect that disallowing e.g. Date::ISO8601 (disallowing on a "used on" class) would also disallow DateTimeImmutable::ISO8601, which unfortunately wouldn't be the case.

Second, disallowing constants doesn't support wildcards. The only real-world use case I could think of is the Date*::CONSTANT case and that can be easily solved by disallowing DateTimeInterface::CONSTANT already.

Last but not least, class constants have to be specified using two keys: class and constant:

parameters:
    disallowedConstants:
        -
            class: 'DateTimeInterface'
            constant: 'ISO8601'
            message: 'use DateTimeInterface::ATOM instead'

Using the fully-qualified name would result in the constant being replaced with its actual value. Otherwise, the extension would see constant: "Y-m-d\TH:i:sO" instead of constant: DateTimeInterface::ISO8601 for example.

Example output

 ------ --------------------------------------------------------
  Line   libraries/Report/Processor/CertificateTransparency.php
 ------ --------------------------------------------------------
  116    Calling var_dump() is forbidden, use logger instead
 ------ --------------------------------------------------------

Allow some previously disallowed calls or attributes

Sometimes, the method, the function, or the constant needs to be called or used once in your code, for example in a custom wrapper. You can use PHPStan's ignoreErrors feature to ignore that one call:

ignoreErrors:
    -
        message: '#^Calling Redis::connect\(\) is forbidden, use our own Redis instead#'  # Needed for the constructor
        path: application/libraries/Redis/Redis.php
    -
        message: '#^Calling print_r\(\) is forbidden, use logger instead#'  # Used with $return = true
        paths:
            - application/libraries/Tls/Certificate.php
            - application/libraries/Tls/CertificateSigningRequest.php
            - application/libraries/Tls/PublicKey.php

You can also allow some previously disallowed calls and usages using the allowIn configuration key, for example:

parameters:
    disallowedMethodCalls:
        -
            method: 'PotentiallyDangerous\Logger::log()'
            message: 'use our own logger instead'
            allowIn:
                - path/to/some/file-*.php
                - tests/*.test.php

Paths in allowIn support fnmatch() patterns.

Relative paths in allowIn are resolved based on the current working directory. When running PHPStan from a directory or subdirectory which is not your "root" directory, the paths will probably not work. Use filesRootDir in that case to specify an absolute root directory for all allowIn paths. Absolute paths might change between machines (for example your local development machine and a continuous integration machine) but you can use %rootDir% to start with PHPStan's root directory (usually /something/something/vendor/phpstan/phpstan) and then .. from there to your "root" directory.

For example when PHPStan is installed in /home/foo/vendor/phpstan/phpstan and you're using a configuration like this:

parameters:
    filesRootDir: %rootDir%/../../..
    disallowedMethodCalls:
        -
            method: 'PotentiallyDangerous\Logger::log()'
            allowIn:
                - path/to/some/file-*.php

then Logger::log() will be allowed in /home/foo/path/to/some/file-bar.php.

If you need to disallow a methods or a function call, a constant, a namespace, a class, a superglobal, or an attribute usage only in certain paths, as an inverse of allowIn, you can use allowExceptIn (or the disallowIn alias):

parameters:
    filesRootDir: %rootDir%/../../..
    disallowedMethodCalls:
        -
            method: 'PotentiallyDangerous\Logger::log()'
            allowExceptIn:
                - path/to/some/dir/*.php

This will disallow PotentiallyDangerous\Logger::log() calls in %rootDir%/../../../path/to/some/dir/*.php.

Please note that before version 2.15, filesRootDir was called allowInRootDir which is still supported, but deprecated.

To allow a previously disallowed method or function only when called from a different method or function in any file, use allowInFunctions (or allowInMethods alias):

parameters:
    disallowedMethodCalls:
        -
            method: 'PotentiallyDangerous\Logger::log()'
            message: 'use our own logger instead'
            allowInMethods:
                - Foo\Bar\Baz::method()

And vice versa, if you need to disallow a method or a function call only when done from a particular method or function, use allowExceptInFunctions (with aliases allowExceptInMethods, disallowInFunctions, disallowInMethods):

parameters:
    disallowedMethodCalls:
        -
            method: 'Controller::redirect()'
            message: 'redirect in startup() instead'
            allowExceptInMethods:
                - Controller\Foo\Bar\*::__construct()

The function or method names support fnmatch() patterns.

Allow with specified parameters only

You can also narrow down the allowed items when called with some parameters (applies only to disallowed method, static & function calls, for obvious reasons). Please note that for now, only scalar values are supported in the configuration, not arrays.

For example, you want to disallow calling print_r() but want to allow print_r(..., true). This can be done with optional allowParamsInAllowed or allowParamsAnywhere configuration keys:

parameters:
    disallowedMethodCalls:
        -
            method: 'PotentiallyDangerous\Logger::log()'
            message: 'use our own logger instead'
            allowIn:
                - path/to/some/file-*.php
                - tests/*.test.php
            allowParamsInAllowed:
                -
                    position: 1
                    name: 'message'
                    value: 'foo'
                -
                    position: 2
                    name: 'alert'
                    value: true
            allowParamsAnywhere:
                -
                    position: 2
                    name: 'alert'
                    value: true

When using allowParamsInAllowed, calls will be allowed only when they are in one of the allowIn paths, and are called with all parameters listed in allowParamsInAllowed. With allowParamsAnywhere, calls are allowed when called with all parameters listed no matter in which file. In the example above, the log() method will be disallowed unless called as:

  • log(..., true) (or log(..., alert: true)) anywhere
  • log('foo', true) (or log(message: 'foo', alert: true)) in another/file.php or optional/path/to/log.tests.php

Use allowParamsInAllowedAnyValue and allowParamsAnywhereAnyValue if you don't care about the parameter's value but want to make sure the parameter is passed. Following the previous example:

parameters:
    disallowedMethodCalls:
        -
            method: 'PotentiallyDangerous\Logger::log()'
            message: 'use our own logger instead'
            allowIn:
                - path/to/some/file-*.php
                - tests/*.test.php
            allowParamsInAllowedAnyValue:
                -
                    position: 2
                    name: 'alert'
            allowParamsAnywhereAnyValue:
                -
                    position: 1
                    name: 'message'

means that you should use (... means any value):

  • log(...) (or log(message: ...)) anywhere
  • log(..., ...) (or log(message: ..., alert: ...)) in another/file.php or optional/path/to/log.tests.php

Such configuration only makes sense when both the parameters of log() are optional. If they are required, omitting them would result in an error already detected by PHPStan itself.

Allow calls except when a param has a specified value

Sometimes, it's handy to disallow a function or a method call only when a parameter matches a configured value but allow it otherwise. Please note that currently only scalar values are supported, not arrays.

For example the hash() function, it's fine using it with algorithm families like SHA-2 & SHA-3 (not for passwords though) but you'd like PHPStan to report when it's used with MD5 like hash('md5', ...). You can use allowExceptParams (or disallowParams), allowExceptCaseInsensitiveParams (or disallowCaseInsensitiveParams), allowExceptParamsInAllowed (or disallowParamsInAllowed) config options to disallow only some calls:

parameters:
    disallowedFunctionCalls:
        -
            function: 'hash()'
            allowExceptCaseInsensitiveParams:
                -
                    position: 1
                    name: 'algo'
                    value: 'md5'

This will disallow hash() call where the first parameter (or the named parameter algo) is 'md5'. allowExceptCaseInsensitiveParams is used because the first parameter of hash() is case-insensitive (so you can also use 'MD5', or even 'Md5' & 'mD5' if you wish). To disallow only exact matches, use allowExceptParams:

parameters:
    disallowedFunctionCalls:
        -
            function: 'foo()'
            allowExceptParams:
                -
                    position: 2
                    value: 'baz'

will disallow foo('bar', 'baz') but not foo('bar', 'BAZ').

It's also possible to disallow functions and methods previously allowed by path (using allowIn) or by function/method name (allowInMethods) when they're called with specified parameters, and allow when called with any other parameter. This is done using the allowExceptParamsInAllowed config option.

Take this example configuration:

parameters:
    disallowedFunctionCalls:
        -
            function: 'waldo()'
            allowIn:
                - 'views/*'
            allowExceptParamsInAllowed:
                -
                    position: 2
                    value: 'quux'

Calling waldo() is disallowed, and allowed back again only when the file is in the views/ subdirectory and waldo() is called in the file with a 2nd parameter being the string quux.

As already demonstrated above, named parameters are also supported:

parameters:
    disallowedFunctionCalls:
        -
            function: 'json_decode()'
            message: 'set the $flags parameter to `JSON_THROW_ON_ERROR` to throw a JsonException'
            allowParamsAnywhere:
                -
                    position: 4
                    name: 'flags'
                    value: ::JSON_THROW_ON_ERROR

This format allows to detect the value in both cases whether it's used with a traditional positional parameter (e.g. json_decode($foo, null, 512, JSON_THROW_ON_ERROR)) or a named parameter (e.g. json_decode($foo, flags: JSON_THROW_ON_ERROR)). All keys are optional but if you don't specify name, the named parameter will not be found in a call like e.g. json_decode($foo, null, 512, JSON_THROW_ON_ERROR). And vice versa, if you don't specify the position key, only the named parameter will be found matching this definition, not the positional one.

You can use shortcuts like

parameters:
    disallowedFunctionCalls:
            # ...
            allowParamsAnywhere:
                2: true
                foo: 'bar'
            allowParamsAnywhereAnyValue:
                - 2
                - foo

which internally expands to

parameters:
    disallowedFunctionCalls:
            # ...
            allowParamsAnywhere:
                -
                    position: 2
                    value: true
                -
                    name: foo
                    value: 'bar'
            allowParamsAnywhereAnyValue:
                -
                    position: 2
                -
                    name: foo

But because the "positional or named" limitation described above applies here as well, I generally don't recommend using these shortcuts and instead recommend specifying both position and name keys.

Allow with specified parameter flags only

Some functions can be called with flags or bitmasks, for example

json_encode($foo, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT);

Let's say you want to disallow json_encode() except when called with JSON_HEX_APOS (integer 4) flag. In the call above, the value of the second parameter (JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) is 13 (1 | 4 | 8). For the extension to be able to "find" the 4 in 13, you need to use the ParamFlags family of config options:

  • allowParamFlagsInAllowed
  • allowParamFlagsAnywhere
  • allowExceptParamFlagsInAllowed or disallowParamFlagsInAllowed
  • allowExceptParamFlags or disallowParamFlags

They work like their non-flags Param counterparts except they're looking if specific bits in the mask parameter are set.

The json_encode() example mentioned above would look like the following snippet:

parameters:
    disallowedFunctionCalls:
            function: 'json_encode'
            allowParamFlagsAnywhere:
                -
                    position: 2
                    value: ::JSON_HEX_APOS

Allowing previously disallowed attributes

Disallowed PHP attributes can be allowed again using the same configuration as what methods and functions use. For example, to require #[Entity] attribute to always specify $repositoryClass argument, you can use configuration similar to the one below. First, we disallow all #[Entity] attributes, then re-allow them only if they contain the parameter (with any value):

parameters:
    disallowedAttributes:
        -
            attribute: Entity
            message: 'you must specify $repositoryClass parameter with Entity'
            allowParamsAnywhereAnyValue:
                -
                    position: 1
                    name: repositoryClass

Case-(in)sensitivity

Function names, method names, class names, namespaces are matched irrespective of their case (disallowing print_r will also find print_R calls), while anything else like constants, file names, paths are not.

Detect disallowed calls without any other PHPStan rules

If you want to use this PHPStan extension without running any other PHPStan rules, you can use phpstan.neon config file that looks like this (the customRulesetUsed: true and the missing level key are the important bits):

parameters:
    customRulesetUsed: true
includes:
    - vendor/spaze/phpstan-disallowed-calls/extension.neon
    - vendor/spaze/phpstan-disallowed-calls/disallowed-dangerous-calls.neon
    - vendor/spaze/phpstan-disallowed-calls/disallowed-execution-calls.neon

Running tests

If you want to contribute (awesome, thanks!), you should add/run tests for your contributions. First install dev dependencies by running composer install, then run PHPUnit tests with composer test, see scripts in composer.json. Tests are also run on GitHub with Actions on each push.

You can fix coding style issues automatically by running composer cs-fix.

See also

There's a similar project with a slightly different configuration, created almost at the same time (just a few days difference): PHPStan Banned Code.

Framework or package-specific configurations

More Repositories

1

hashes

Magic hashes – PHP hash "collisions"
614
star
2

oprah-proxy

Generate credentials for Opera's "browser VPN"
Python
255
star
3

domains

Unofficial and incomplete lists of various domain names
69
star
4

jakobejitblokaci.cz

www.jakobejitblokaci.cz
HTML
42
star
5

letsgetacert

Let's get cert – a Certbot wrapper
Shell
23
star
6

encrypt-hash-password-php

Example of an encrypted password hash storage in PHP
PHP
21
star
7

upc_keys-lambda

Peter "blasty" Geissler's upc_keys.c with custom prefix support and Lambda sauce
C
15
star
8

webtop100

Dokumenty k hodnocení soutěže WebTop100
15
star
9

csp-config

Build Content Security Policy from a config file
PHP
11
star
10

exploited.cz

https://exploited.cz
HTML
8
star
11

michalspacek.cz

michalspacek.cz + michalspacek.com + subdomains source code because why not
PHP
8
star
12

nonce-generator

Content Security Policy Nonce Generator
PHP
6
star
13

ebabis.cz

A v březnu někdo přišel s tím matematickým modelem A v srpnu někdo, sice byl to ten stejný člověk, ale už přišel v nějakém čase A ty, který měli přijít, nepřišli
HTML
6
star
14

canhas.report

Reporting API demos, learn all about CSP & other reports (Network Error Logging/NEL, Crash, Deprecation, Intervention, Mixed Content & more) in this interactive app.
PHP
6
star
15

phpstan-stripe

Stripe SDK extension for PHPStan
PHP
5
star
16

emulated-prepared-statements

SQL Injection using Emulated Prepared Statements (the default) in PHP PDO_MYSQL in GBK
PHP
5
star
17

cotel

Company Intel, data and proof-of-concept quick-and-dirty code for an bookmarking app. Abandoned.
CSS
5
star
18

phpinfo

Extract phpinfo() into a variable and move CSS to external file
CSS
4
star
19

sri-macros

Subresource Integrity macros for Latte template engine
PHP
4
star
20

bez-komentare

Filtry odstraňující komentáře, jejich počet, notifikace z českých stránek
3
star
21

fa-extract

A tool to extract only used icons from Font Awesome JS/SVG icons sets (PRE-ALPHA, I use it but YMMV)
PHP
3
star
22

feed-exports

Atom feed Response and related objects for Nette framework
PHP
3
star
23

svg-icons-latte

SVG Icons Custom Tag for Latte Templating System
PHP
2
star
24

coding-standard

PHP Code Sniffer rules
PHP
2
star
25

security-txt

PHP
2
star
26

stupid-git-deploy

Shell
2
star
27

nepovolenainternetovahazardnihra.cz

nepovolenainternetovahazardnihra.cz + jakobejitblokaci.cz = BFF ❤
HTML
2
star
28

libini-djgpp

LibINI is a C library for DJGPP and Linux for reading, updating and writing Windows-like INI files. I wrote this back in 1999.
C
2
star
29

reveal-input-details

Chrome JS snippet to reveal hidden inputs and input details
JavaScript
1
star
30

webleed

We Bleed scanner tools
Python
1
star
31

svnwcdump

Dumps Subversion working copy located at a website and accessible using HTTP
Python
1
star
32

phpstan-disallowed-calls-nette

1
star
33

common-config

Configuration wants to be shared
1
star