denzyl / phanalist
Performant static analyzer for PHP, which is extremely easy to use. It helps you catch common mistakes in your PHP code.
Requires
None
Requires (Dev)
None
Suggests
- ext-curl: Recommended for downloading the binary
- ext-zip: Required to extract the binary on Windows
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v1.2.2
- v1.2.1
- v1.2.0
- v1.1.15
- v1.1.14
- v1.1.13
- v1.1.12
- v1.1.11
- v1.1.10
- v1.1.9
- v1.1.8
- v1.1.7
- v1.1.6
- v1.1.5
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.1
- v1.0.0
- v0.1.32
- v0.1.31
- v0.1.30
- v0.1.29
- v0.1.28
- v0.1.27
- 0.1.26
- 0.1.25
- 0.1.24
- v0.1.22
- v0.1.21
- v0.1.20
- v0.1.19
- dev-fix/release-v1.2.2-build
- dev-feat/security-taint-markers
- dev-feat/security-taint-mode
This package is auto-updated.
Last update: 2026-11-10 22:11:33 UTC
README
Phanalist
Fast static analyzer for PHP. Finds maintainability and runtime risks with zero configuration required.
What it does
Phanalist scans PHP codebases and reports actionable findings — no configuration required. Each finding explains what's wrong, why it matters, and how to fix it.
It covers:
- Code health — class design, complexity, coupling, method size, naming
- Runtime risk — division by zero, array/string bounds, null dereferences, unreachable code
- Resource usage — loops and allocations that grow with input
- Security — SQL injection, XSS, command injection, weak crypto, unsafe deserialization, header/mail injection, and more
Quickstart
Install:
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/denzyldick/phanalist/main/bin/init.sh | sh
Scan your project:
cd your-php-project ~/phanalist
On first run a phanalist.yaml is created with sensible defaults. Review findings, then integrate into CI:
name: Phanalist on: [pull_request] permissions: security-events: write jobs: phanalist: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: denzyldick/phanalist-action@v1
See Installation for Composer, Docker, and source-build options.
Features
- Fast — built in Rust, analyzes large codebases in seconds
- 136 built-in rules across code health, runtime verification, resource usage, and security
- Zero config — works out of the box; tune via
phanalist.yaml - Multiple output formats — text, JSON, SARIF (GitHub code scanning), Code Climate
- Editor integration — built-in Language Server (
phanalist --lsp) - Engineer quality report — attribute violations to authors via
git blame(--blame) - Baseline — adopt on legacy codebases without fixing everything at once (
--use-baseline)
Configuration
Tune rules in phanalist.yaml:
enabled_rules: [] # empty = all rules active disable_rules: [] exclude_paths: - vendor - node_modules rules: E0007: max_parameters: 8 E0009: max_complexity: 10
All rules are enabled by default; use enabled_rules, disable_rules, and per-rule rules settings in phanalist.yaml to customize them:
- AST & Code Quality: Checks syntax structure, formatting conventions, naming rules, method complexity metrics, and class design.
- Formal Verification: Analyzes control flow to prove runtime safety (such as preventing division by zero, null errors, and array bounds violations).
- Resource Analysis: Measures time complexity, memory allocation growth, and untrusted loop cardinality.
- Lifecycle & Runtime Safety: Checks resource cleanup, runtime-error traps, and reliability hazards.
- Security: Detects security vulnerabilities and dangerous execution sinks.
Security rules that use source-to-sink taint analysis accept a per-rule taint_mode:
annotate(default) keeps the finding and adds the proven source origin.onlyreports only proven tainted flows.offkeeps the legacy heuristic-only behaviour.
The taint pass runs on all sources by default, so library and dependency scans surface dangerous sinks even when the superglobal is in the calling app. Findings are tiered by certainty:
- Proven — reachable from request input (
$_GET,$_POST, …): high confidence, original rule severity, marked[proven]in text output. - Lead — a function parameter reaches a sink, but no caller is shown to
pass untrusted data: low confidence,
minorseverity, marked[lead]. Leads are reported but don't fail the run on their own.
Pass --taint-source-request (or set taint_source_all: false in
phanalist.yaml) to hide leads and keep only request-rooted flows.
📜 Rules Reference
Every rule links to junior-friendly documentation with bad/good examples, "why it matters," and fix instructions. Rule names match the DESCRIPTION constant in each rule's source file exactly.
Section 1: AST & Code Quality Rules (E0000–E0030)
Type: Syntax, metrics, structure, and design checks.
| Code | Name | Primary Option / Threshold |
|---|---|---|
| E0000 | Example rule | — |
| E0001 | Opening tag position | — |
| E0002 | Empty catch | — |
| E0003 | Method modifiers | — |
| E0004 | Uppercase constants | — |
| E0005 | Capitalized class name | — |
| E0006 | Property modifiers | — |
| E0007 | Method parameters count | max_parameters: 8 |
| E0008 | Return type signature | — |
| E0009 | Cyclomatic complexity | max_complexity: 10 |
| E0010 | Npath complexity | max_paths: 200 |
| E0011 | Detect the error suppression symbol: @ | — |
| E0012 | Service compatibility with Shared Memory Model | include_namespaces, reset_interfaces |
| E0013 | Private method not being called. | — |
| E0014 | Law of Demeter violation. Method chaining should be avoided unless returning the same object type. | — |
| E0015 | Lack of Cohesion of Methods (LCOM4) | threshold: 1 |
| E0016 | Cognitive complexity | max_complexity: 15 |
| E0017 | Coupling Between Objects (CBO) | max_coupling: 10 |
| E0018 | Weighted Methods per Class (WMC) | max_wmc: 50 |
| E0019 | Response For a Class (RFC) | max_rfc: 50 |
| E0020 | Depth of Inheritance Tree (DIT) | max_depth: 4 |
| E0021 | Number of Children (NOC) | max_children: 15 |
| E0022 | Afferent and Efferent Coupling (Ca/Ce) | max_ca: 20, max_ce: 20 |
| E0023 | Instability (I), Abstractness (A), Distance from Main Sequence (D) | max_distance: 0.5 |
| E0024 | Lines of Code (LOC) per Method | max_loc: 50 |
| E0025 | Lines of Code (LOC) per File | max_loc: 500 |
| E0026 | Comment Ratio | min_ratio: 0.05, max_ratio: 0.5 |
| E0027 | God Class (Brain Class) | max_methods: 15, max_fields: 10 |
| E0028 | Data Class | min_methods: 3 |
| E0029 | Class-level Fan-in / Fan-out | max_fan_out: 10, max_fan_in: 20 |
| E0030 | Cyclomatic Complexity Density | max_density: 0.3 |
Section 2: Formal Verification Rules (E0031–E0043)
Type: Path analysis and runtime safety checks.
| Code | Name | What it Proves / Checks |
|---|---|---|
| E0031 | Division by Zero | Proves that divisors are never zero |
| E0032 | Modulo by Zero | Proves that modulo divisors are never zero |
| E0033 | intdiv() by Zero |
Proves integer division divisors are valid |
| E0034 | intdiv() Overflow |
Prevents integer overflow during division |
| E0035 | Missing Array Key | Proves required array keys exist before access |
| E0036 | Array Index Out of Bounds | Proves array index access stays within bounds |
| E0037 | String Offset Out of Bounds | Proves string character offsets are valid |
| E0038 | Method Call on Null | Proves objects are not null before calling methods |
| E0039 | Property Access on Null | Proves objects are not null before accessing properties |
| E0040 | Return Contract Violation | Verifies return types match documented contracts |
| E0041 | Unreachable Code | Detects dead statements after returns or throws |
| E0042 | Impossible Assertion | Detects assertions that can never hold true |
| E0043 | Stream Resource Used After Close | Detects reading/writing streams after closing them |
Section 3: Resource Analysis Rules (E0044–E0046)
Type: Scalability, memory growth, and time complexity checks.
| Code | Name | What it Analyzes |
|---|---|---|
| E0044 | Input-Dependent Runtime Growth | Flags loops whose time complexity scales poorly with input size |
| E0045 | Input-Dependent Live-Heap Growth | Flags memory allocation patterns that grow with input size |
| E0046 | Input-Controlled Cardinality | Flags collections sized directly from untrusted user input |
Section 4: Security & Lifecycle Rules (E0047–E0135)
Type: Security vulnerabilities and dangerous execution sinks.
E0047–E0052 — Resource lifecycle & runtime safety
| Code | Name | Risk |
|---|---|---|
| E0047 | Resource lifecycle leak | Unclosed file handles or database connections |
| E0048 | Unbounded direct recursion | Infinite recursion without base condition changes |
| E0049 | Invalid builtin argument | Invalid literal arguments passed to built-in functions |
| E0050 | Position check used as boolean | Using strpos() without strict comparison (!== false) |
| E0051 | Count of a non-countable value | Calling count() on non-array scalars |
| E0052 | Unchecked json_decode result | Missing validation after JSON decoding |
E0053–E0059 — Security hazards
| Code | Name | Security Risk |
|---|---|---|
| E0053 | Unserialize without allowed_classes | PHP object injection risk |
| E0054 | Dynamic variable creation | Variable variables ($$x) or extract() usage |
| E0055 | Dynamic include or require | Unsafe dynamic file inclusion paths |
| E0056 | Weak randomness for security-sensitive value | Using rand() or mt_rand() for security tokens |
| E0057 | MD5 or SHA1 used for a password | Insecure hashing algorithms for credentials |
| E0058 | Cookie missing Secure or HttpOnly | Cookies missing security flags |
| E0059 | Dynamic eval or assert | Dangerous code execution sinks (eval()) |
E0060–E0069 — Injection attacks
| Code | Name | Security Risk |
|---|---|---|
| E0060 | SQL injection | User input concatenated into SQL queries |
| E0061 | Cross-Site Scripting (XSS) | Unescaped user input echoed to the browser |
| E0062 | Command injection | User input passed to shell execution functions |
| E0063 | LDAP injection | User input embedded in LDAP filters |
| E0064 | XPath injection | User input embedded in XPath queries |
| E0065 | NoSQL injection | User input embedded in NoSQL queries |
| E0066 | Header injection | User input written to HTTP headers (CRLF injection) |
| E0067 | Mail injection | User input written to mail headers or subject |
| E0068 | Eval injection | User input evaluated as PHP code via eval() |
| E0069 | Unsafe deserialization | Untrusted data passed to unserialize() |
E0070–E0079 — File, session, and crypto risks
| Code | Name | Security Risk |
|---|---|---|
| E0070 | Remote file inclusion | User-controlled file included via include/require |
| E0071 | Unsafe file upload | Uploaded file stored to user-controlled destination |
| E0072 | Path traversal | User-controlled file path opened for read/write |
| E0073 | Weak cryptographic hash | MD5/SHA1 used instead of password_hash() |
| E0074 | Weak random number generator | mt_rand()/rand() used for security values |
| E0075 | Session security | session_start() without secure cookie settings |
| E0076 | Information disclosure | phpinfo() left in production code |
| E0077 | Display errors enabled | display_errors enabled via ini_set() |
| E0078 | Variable variables | extract() importing user-controlled variables |
| E0079 | Dynamic method call | Dynamic method invoked on user-controlled object |
E0080–E0089 — Deprecated functions & injection
| Code | Name | Security Risk |
|---|---|---|
| E0080 | Weak cryptographic function | crypt() used for password hashing |
| E0081 | Deprecated regex function | ereg/split used instead of preg_* |
| E0082 | Dynamic extension loading | dl() loading extensions at runtime |
| E0083 | Open redirect | header() redirect to user-controlled URL |
| E0084 | SQL query string concatenation | SQL query built by concatenating user input |
| E0085 | Inline PHP execution short tags | <?= ?> short echo tag usage |
| E0086 | Deprecated session function | session_register()/session_is_registered() usage |
| E0087 | Command execution with backticks | Backtick command with interpolated input |
| E0088 | Reflection-based injection | Reflection invoked on user-controlled input |
| E0089 | Log injection | User input written to log files unsanitized |
E0090–E0099 — File, XML, and cookie security
| Code | Name | Security Risk |
|---|---|---|
| E0090 | File write to user-controlled path | file_put_contents() to user-controlled path |
| E0091 | Source code exposure via highlight_file | highlight_file()/show_source() in production |
| E0092 | XML External Entity (XXE) | XML parsed from user-controlled input |
| E0093 | Template injection | User input rendered as a template |
| E0094 | Denial of service via regular expression | Regex applied to user-controlled input |
| E0095 | Insecure comparison | strcmp()/strcasecmp() not constant-time |
| E0096 | Cookie without secure flag | Cookie set without the Secure flag |
| E0097 | Cookie without HttpOnly flag | Cookie set without the HttpOnly flag |
| E0098 | Session ID in URL | Session ID embedded in redirect URL |
| E0099 | Disable functions bypass | ini_set() changing disable_functions |
E0100–E0109 — Dynamic code & SSRF
| Code | Name | Security Risk |
|---|---|---|
| E0100 | Variable function call | Variable function invoked on user-controlled name |
| E0101 | Object injection via unserialize | Object injection from unserialize of user input |
| E0102 | Command injection via proc_open | proc_open() with user-controlled command |
| E0103 | Server-side request forgery | Network request to user-controlled URL |
| E0104 | HTTP response splitting | header() with user-controlled CRLF sequences |
| E0105 | Local file inclusion | Local file included from user-controlled input |
| E0106 | Deserialization of untrusted data | unserialize()/wddx_deserialize() of user input |
| E0107 | Weak random for security tokens | Weak random used for security-sensitive tokens |
| E0108 | Dynamic instantiation | Class instantiated from user-controlled name |
| E0109 | Directory listing exposure | opendir() on user-controlled directory |
E0110–E0119 — SQL, XML, and session security
| Code | Name | Security Risk |
|---|---|---|
| E0110 | Raw SQL query execution | Raw SQL query executed without parameters |
| E0111 | Deprecated mysql extension | Removed ext/mysql extension functions used |
| E0112 | File upload without validation | move_uploaded_file() without destination validation |
| E0113 | Session regeneration missing | Session ID not regenerated after authentication |
| E0114 | PHP info disclosure | phpinfo() exposing server configuration |
| E0115 | XXE via libxml | XML parsed by libxml from user-controlled input |
| E0116 | Remote command execution via popen | popen() with user-controlled command |
| E0117 | SQL injection via query string | SQL query string built from user input |
| E0118 | Log forging via user input | User input written to logs without sanitization |
| E0119 | Session cookie without Secure flag | Session cookie set without Secure flag |
E0120–E0135 — Dynamic execution & header injection
| Code | Name | Security Risk |
|---|---|---|
| E0120 | Eval with user-controlled string | eval() executed on user-controlled string |
| E0121 | Assert with user-controlled string | assert() with user-controlled string argument |
| E0122 | File inclusion with user input | include/require with user-controlled path |
| E0123 | XSS via echo | echo() outputs user-controlled input unescaped |
| E0124 | Weak hash for passwords | Weak hash (MD5/SHA1/crypt) used for passwords |
| E0125 | Open redirect via header | header() redirects to user-controlled URL |
| E0126 | Path traversal via file_get_contents | file_get_contents() reads user-controlled path |
| E0127 | Command execution via system | system() with user-controlled command |
| E0128 | Command execution via exec | exec() with user-controlled command |
| E0129 | Command execution via shell_exec | shell_exec() with user-controlled command |
| E0130 | Command execution via passthru | passthru() with user-controlled command |
| E0131 | Command execution via proc_open | proc_open() with user-controlled command |
| E0132 | Header injection via header | header() writes user input to HTTP headers |
| E0133 | Header injection via setcookie | setcookie() writes user input to cookie header |
| E0134 | Mail injection via mail | mail() writes user input to mail headers/subject |
| E0135 | PHP info disclosure | phpinfo() exposing configuration details |
Editor integration
Run Phanalist as a Language Server for real-time diagnostics:
phanalist --lsp
Community
MIT Licensed.
