7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
158 lines
4.9 KiB
Python
Executable File
158 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Pre-commit hook to enforce safe_requests usage for SSRF protection.
|
|
|
|
This prevents direct usage of requests.get/post/Session which bypasses
|
|
SSRF validation. Use safe_get/safe_post/SafeSession from the security module.
|
|
|
|
See: https://cwe.mitre.org/data/definitions/918.html (CWE-918: SSRF)
|
|
"""
|
|
|
|
import ast
|
|
import sys
|
|
|
|
|
|
# Files/patterns where direct requests usage is allowed
|
|
ALLOWED_PATTERNS = {
|
|
"security/safe_requests.py", # The wrapper itself must use raw requests
|
|
"tests/", # Test files may need to mock/test raw requests
|
|
"test_", # Test files
|
|
"_test.py", # Test files
|
|
"examples/", # Example scripts run client-side, not server-side
|
|
}
|
|
|
|
# HTTP methods that should use safe_get/safe_post
|
|
UNSAFE_METHODS = {"get", "post", "put", "delete", "patch", "head", "options"}
|
|
|
|
|
|
class RequestsChecker(ast.NodeVisitor):
|
|
"""AST visitor to detect unsafe requests usage."""
|
|
|
|
def __init__(self, filename: str):
|
|
self.filename = filename
|
|
self.errors = []
|
|
|
|
def visit_Call(self, node):
|
|
# Skip if file is in allowed list
|
|
if self._is_file_allowed():
|
|
return self.generic_visit(node)
|
|
|
|
# Pattern 1: requests.get(), requests.post(), etc.
|
|
if (
|
|
isinstance(node.func, ast.Attribute)
|
|
and node.func.attr in UNSAFE_METHODS
|
|
and isinstance(node.func.value, ast.Name)
|
|
and node.func.value.id == "requests"
|
|
):
|
|
method = node.func.attr
|
|
if method in ("get", "post"):
|
|
safe_method = f"safe_{method}"
|
|
suggestion = (
|
|
f"Use {safe_method}() from security module instead."
|
|
)
|
|
else:
|
|
suggestion = (
|
|
"Use SafeSession() from security module for HTTP requests."
|
|
)
|
|
|
|
self.errors.append(
|
|
(
|
|
node.lineno,
|
|
f"Direct requests.{method}() bypasses SSRF protection. "
|
|
f"{suggestion}",
|
|
)
|
|
)
|
|
|
|
# Pattern 2: requests.Session()
|
|
elif (
|
|
isinstance(node.func, ast.Attribute)
|
|
and node.func.attr == "Session"
|
|
and isinstance(node.func.value, ast.Name)
|
|
and node.func.value.id == "requests"
|
|
):
|
|
self.errors.append(
|
|
(
|
|
node.lineno,
|
|
"Direct requests.Session() bypasses SSRF protection. "
|
|
"Use SafeSession() from security module instead.",
|
|
)
|
|
)
|
|
|
|
self.generic_visit(node)
|
|
|
|
def _is_file_allowed(self) -> bool:
|
|
"""Check if this file is allowed to use requests directly."""
|
|
for pattern in ALLOWED_PATTERNS:
|
|
if pattern in self.filename:
|
|
return True
|
|
return False
|
|
|
|
|
|
def check_file(filename: str) -> bool:
|
|
"""Check a single Python file for unsafe requests usage."""
|
|
if not filename.endswith(".py"):
|
|
return True
|
|
|
|
try:
|
|
with open(filename, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
except Exception as e:
|
|
print(f"Error reading {filename}: {e}")
|
|
return False
|
|
|
|
try:
|
|
tree = ast.parse(content, filename=filename)
|
|
checker = RequestsChecker(filename)
|
|
checker.visit(tree)
|
|
|
|
if checker.errors:
|
|
print(f"\n{filename}:")
|
|
for line_num, error in checker.errors:
|
|
print(f" Line {line_num}: {error}")
|
|
return False
|
|
|
|
except SyntaxError:
|
|
# Skip files with syntax errors (let other tools handle that)
|
|
pass
|
|
except Exception as e:
|
|
print(f"Error parsing {filename}: {e}")
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def main():
|
|
"""Main function to check all provided Python files."""
|
|
if len(sys.argv) < 2:
|
|
print("Usage: check-safe-requests.py <file1> <file2> ...")
|
|
sys.exit(1)
|
|
|
|
files_to_check = sys.argv[1:]
|
|
has_errors = False
|
|
|
|
for filename in files_to_check:
|
|
if not check_file(filename):
|
|
has_errors = True
|
|
|
|
if has_errors:
|
|
print("\n" + "=" * 70)
|
|
print("SSRF Protection: Unsafe requests usage detected!")
|
|
print("=" * 70)
|
|
print("\nTo fix, replace direct requests calls with safe alternatives:")
|
|
print(" - requests.get() -> safe_get()")
|
|
print(" - requests.post() -> safe_post()")
|
|
print(" - requests.Session() -> SafeSession()")
|
|
print("\nImport from security module:")
|
|
print(" from ...security import safe_get, safe_post, SafeSession")
|
|
print("\nFor localhost/internal services, use:")
|
|
print(" safe_get(url, allow_localhost=True)")
|
|
print(" safe_get(url, allow_private_ips=True)")
|
|
print("\nSee: https://cwe.mitre.org/data/definitions/918.html")
|
|
sys.exit(1)
|
|
else:
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|