Security Rules¶
Security rules detect patterns that may introduce security vulnerabilities into your codebase. These rules focus on finding credentials, secrets, and other sensitive data that should never be hardcoded.
Scope limitation
These security rules detect only direct superglobal usage patterns ($_GET, $_POST, etc.). They do NOT perform taint analysis or track data flow through variables. For deeper security analysis with taint tracking, consider dedicated tools like PHPStan Security, Psalm Taint Analysis, or SonarQube.
Hardcoded Credentials¶
Rule ID: security.hardcoded-credentials
Severity: Error
What it measures¶
Detects hardcoded credentials in PHP code -- string literal values assigned to variables, properties, constants, array keys, and parameters with credential-related names.
Detection patterns:
- Variable assignment:
$password = 'secret'; - Property assignment:
$this->apiKey = '...';,self::$secret = '...';(also with??=) - Array element assignment:
$config['password'] = '...'; - Array item:
['api_key' => 'abc123'] - Class constant:
const DB_PASSWORD = 'root'; - Namespace or global constant:
const API_PASSWORD = 'root';(including several constants in one declaration) define()call:define('API_KEY', '...');- Property default:
private string $token = 'x'; - Parameter default:
function f($pwd = 'root')
Sensitive name matching:
- Suffix words (match anywhere):
password,passwd,pwd,secret,credential(s) - Compound "key" (only with qualifier):
apiKey,secretKey,privateKey,encryptionKey,signingKey,authKey,accessKey - Compound "token" (only with qualifier):
authToken,accessToken,bearerToken,apiToken,refreshToken
Names are split into words at case changes, underscores and letter/digit boundaries (apiKey1 is api, key, 1). A word written without any boundary is split when it ends with a sensitive word: $apikey, $dbpassword, APISECRET match, while passwordless, secretary, keyword and monkey do not.
Names like $passwordHash, $tokenStorage, $cacheKey, OPTION_PASSWORD are excluded (non-credential context).
Value filtering: these values are skipped:
- empty strings, strings shorter than 4 characters, and strings of identical characters (
***,xxx); - whole angle-bracket placeholders such as
<password>and native built-in PHP type syntax such as?stringorint|string. The type filter parses syntax against a built-in whitelist; it does not validate PHP type semantics; - lowercase dotted identifiers such as
auth.password.resetorauth.password-reset(translation, configuration or channel keys). Uppercase dotted values such asApp.Models.User, capitalized or digit-bearing hyphen segments such asSummer-2024.Passandsk-live.abc123-def456, and JWT-shaped values are judged as possible secrets. A lowercase dotted secret phrase such ascorrect-horse.battery-staplecan still be skipped; - messages: longer than 20 characters and at least three whitespace-separated words. Hyphens, dots, slashes and plus signs are not word breaks, so UUID-, AWS- and base64-shaped keys are still reported.
Bare token8 is outside the default sensitive-name policy; configure token as an extra sensitive name if that name should be judged. A credential-looking value in an alias map may still be reported; suppress an intentional case with a reasoned @qmx-ignore.
Findings inside named classes, methods, and functions carry their declaration symbol. File-scope and anonymous evidence retain a file symbol and null namespace. Direct superglobal checks recognize literal variable-variable names and literal $GLOBALS keys; an unknown dynamic name such as $$name is outside their finite pattern set.
Example¶
class DatabaseConfig
{
// Bad: credentials hardcoded directly
private const DB_PASSWORD = 'super_secret_123';
private string $apiKey = 'sk-live-abc123def456';
public function connect(string $password = 'root'): void
{
$apiToken = 'ghp_xxxxxxxxxxxxxxxxxxxx';
// ...
}
}
How to fix¶
-
Use environment variables:
-
Use a secrets manager (Vault, AWS Secrets Manager, etc.)
-
Use framework configuration:
Warning
Hardcoded credentials in source code are a serious security risk. They can be leaked through version control, logs, error messages, or compiled artifacts.
SQL Injection¶
Rule ID: security.sql-injection
Severity: Error
What it measures¶
Detects use of superglobals ($_GET, $_POST, $_REQUEST, $_COOKIE) in SQL contexts, where unsanitized user input can lead to SQL injection attacks.
Detection patterns:
- String concatenation with SQL keywords:
"SELECT * FROM users WHERE id = " . $_GET['id'] - Arguments to unsafe query functions:
mysql_query($_GET['q']),mysqli_query($conn, $_POST['sql']),pg_query($_REQUEST['q']) sprintf()with SQL template:sprintf("SELECT * FROM users WHERE id = %s", $_GET['id'])
The superglobal is found through the same value-passing wrappers as for XSS ("... WHERE id = " . ($_GET['id'] ?? 0) is reported); a function call or an (int)/(float) cast ends the search.
One query is one finding, however many superglobals it reads. When a query function, a sprintf() call or a concatenation reports a superglobal, the queries built inside it are reported again only for a read it did not reach: mysqli_query($conn, "SELECT * FROM users WHERE name = '{$_POST['name']}'") is one violation, not one for the call and one for the interpolated string. A query behind any other call is still reported on its own: mysqli_query($conn, trim("SELECT * FROM users WHERE id = " . $_GET['id'])) is reported for the concatenation, and in "SELECT * FROM t WHERE a = " . $_GET['a'] . " AND b IN (" . implode(',', ["SELECT id FROM u WHERE n = '{$_POST['n']}'"]) . ")" the subquery is reported for $_POST next to the outer query's $_GET.
Example¶
// Bad: superglobal directly in SQL query
$result = mysqli_query($conn, "SELECT * FROM users WHERE id = " . $_GET['id']);
// Bad: superglobal as argument to query function
$result = pg_query("SELECT * FROM orders WHERE status = '" . $_POST['status'] . "'");
// Bad: sprintf with unsanitized input
$sql = sprintf("DELETE FROM sessions WHERE token = '%s'", $_COOKIE['session']);
How to fix¶
Use parameterized queries (prepared statements) instead of string concatenation:
// Good: PDO prepared statement
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
// Good: mysqli prepared statement
$stmt = $conn->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $_GET['id']);
$stmt->execute();
Warning
SQL injection is one of the most dangerous and common web vulnerabilities. Never concatenate user input into SQL strings, even if you think the input is "safe."
XSS¶
Rule ID: security.xss
Severity: Error
What it measures¶
Detects echo or print statements that output superglobals ($_GET, $_POST, $_REQUEST, $_COOKIE) without proper sanitization, which can lead to Cross-Site Scripting (XSS) attacks.
The superglobal is found through the wrappers that pass its value on: concatenation, interpolation, ??, the branches of ?:, match arm results, (string), @ and assignment — echo $_GET['name'] ?? 'guest'; is reported.
A violation is not reported when the value passes through a function or method call or an (int)/(float) cast. That covers the sanitizers — htmlspecialchars(), htmlentities(), strip_tags(), intval() — but equally every other call: function results are not tracked (see What is NOT detected under Detection scope below).
Example¶
// Bad: unsanitized superglobal echoed directly
echo $_GET['name'];
print("Welcome, " . $_POST['username']);
// Good: sanitized output
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
echo (int) $_GET['page'];
How to fix¶
Always sanitize user input before outputting it in HTML:
// Use htmlspecialchars with ENT_QUOTES and explicit encoding
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
// For integer values, cast to int
echo (int) $_GET['id'];
// In templates, use your framework's auto-escaping (Twig, Blade, etc.)
Warning
XSS allows attackers to inject malicious scripts into pages viewed by other users. Always escape output, even in "internal" admin panels.
Command Injection¶
Rule ID: security.command-injection
Severity: Error
What it measures¶
Detects superglobals ($_GET, $_POST, $_REQUEST, $_COOKIE) passed as arguments to shell execution functions, which can lead to command injection attacks.
Detected sinks: exec(), system(), passthru(), shell_exec(), proc_open(), popen(), and the backtick operator (`ls {$_GET['dir']}`), which runs a command exactly like shell_exec().
The superglobal is found through the same value-passing wrappers as for XSS (??, ?:, (string), concatenation, interpolation, ...). A violation is not reported when the value passes through a function call — escapeshellarg(), escapeshellcmd() and any other — or an (int)/(float) cast.
Example¶
// Bad: superglobal passed directly to shell function
exec("convert " . $_GET['filename'] . " output.png");
system("ping " . $_POST['host']);
$output = shell_exec("grep " . $_REQUEST['pattern'] . " /var/log/app.log");
// Good: properly escaped
exec("convert " . escapeshellarg($_GET['filename']) . " output.png");
How to fix¶
-
Use
escapeshellarg()to escape individual arguments: -
Avoid shell commands entirely when possible. Use PHP built-in functions instead:
Warning
Command injection allows attackers to execute arbitrary commands on your server. Even with escaping, prefer PHP-native alternatives to shell commands when they exist.
Sensitive Parameter¶
Rule ID: security.sensitive-parameter
Severity: Warning
What it measures¶
Detects function and method parameters with sensitive names that are missing the #[\SensitiveParameter] attribute (available since PHP 8.2). Without this attribute, sensitive values like passwords and tokens will appear in plain text in stack traces, error logs, and exception reports.
Sensitive parameter matching uses the same credential-name policy as hardcoded
credential detection: standalone password, passwd, pwd, secret,
credential, and credentials words are matched, while key and token require a qualifying
prefix such as api, auth, access, private, or refresh. Contexts such
as passwordHash, tokenStorage, cacheKey, and OPTION_PASSWORD are
excluded.
Example¶
// Bad: sensitive parameter without the attribute
class AuthService
{
public function login(string $username, string $password): bool
{
// If an exception is thrown here, $password appears in the stack trace
return $this->verify($username, $password);
}
}
// Good: attribute prevents value from appearing in stack traces
class AuthService
{
public function login(
string $username,
#[\SensitiveParameter] string $password,
): bool {
return $this->verify($username, $password);
}
}
How to fix¶
Add the #[\SensitiveParameter] attribute to parameters that handle sensitive data:
function authenticate(
string $username,
#[\SensitiveParameter] string $password,
): bool {
// ...
}
function callApi(
string $url,
#[\SensitiveParameter] string $apiToken,
): Response {
// ...
}
Tip
The #[\SensitiveParameter] attribute was introduced in PHP 8.2. It replaces the value with SensitiveParameterValue in stack traces, preventing accidental exposure in logs and error reports.
Detection scope¶
The security rules (sql-injection, xss, command-injection) use pattern-based detection -- they look for superglobal variables ($_GET, $_POST, $_REQUEST, $_COOKIE) used directly in dangerous contexts. This approach is fast and produces zero false positives for direct patterns, but has inherent limitations.
What IS detected¶
Direct superglobal-to-sink patterns within one expression, including through the wrappers that pass the value on:
// All detected:
echo $_GET['name']; // direct
echo "Hello " . $_POST['user']; // concatenation
echo "Welcome {$_GET['name']}"; // interpolation
echo $_GET['name'] ?? 'guest'; // null coalescing
echo $ok ? $_GET['name'] : ''; // ternary branch
echo (string) $_GET['name']; // string cast
mysqli_query($conn, "SELECT * FROM t WHERE id=" . $_GET['id']); // SQL function arg
exec("ping " . $_GET['host']); // command function arg
$out = `ls {$_GET['dir']}`; // backtick command
What is NOT detected¶
These rules do not perform taint analysis -- they cannot track data flow through variables, function returns, or object properties:
// NOT detected -- value assigned to intermediate variable:
$name = $_GET['name'];
echo $name;
// NOT detected -- value passed through a function:
function getName() { return $_GET['name']; }
echo getName();
// NOT detected -- any function call ends the search, not only a sanitizer:
echo trim($_GET['name']);
// NOT detected -- value stored in an object:
$request->name = $_POST['name'];
echo $request->name;
// NOT detected -- indirect SQL injection:
$id = $_GET['id'];
$query = "SELECT * FROM users WHERE id = " . $id;
Recommendations¶
For comprehensive security analysis with full taint tracking, use dedicated tools alongside Qualimetrix:
- PHPStan Security Advisories -- security-focused PHPStan extension
- Psalm Taint Analysis -- tracks tainted data through assignments, function calls, and returns
- SonarQube -- commercial tool with deep data-flow analysis
- Snyk Code -- AI-powered security scanning with taint tracking
Qualimetrix security rules are best used as a first line of defense to catch the most obvious patterns. They complement but do not replace dedicated security analysis tools.
Configuration¶
# qmx.yaml
rules:
security.hardcoded-credentials:
enabled: true # or false to disable
security.sql-injection:
enabled: true
security.xss:
enabled: true
security.command-injection:
enabled: true
security.sensitive-parameter:
enabled: true
You can also disable via the CLI: