This commit is contained in:
CarterPerez-dev 2025-11-10 13:53:22 -05:00
parent 7b0a43aaea
commit 9dd12f5a31
12 changed files with 283 additions and 195 deletions

View File

@ -16,7 +16,9 @@ class Settings(BaseSettings):
""" """
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
env_file="../.env", env_file_encoding="utf-8", case_sensitive=True env_file = "../.env",
env_file_encoding = "utf-8",
case_sensitive = True
) )
# Application metadata # Application metadata
@ -84,7 +86,9 @@ class Settings(BaseSettings):
""" """
Convert comma separated CORS origins string to list Convert comma separated CORS origins string to list
""" """
return [origin.strip() for origin in self.CORS_ORIGINS.split(",")] return [
origin.strip() for origin in self.CORS_ORIGINS.split(",")
]
@lru_cache @lru_cache
@ -100,4 +104,5 @@ def get_settings() -> Settings:
""" """
return Settings() return Settings()
settings = get_settings() settings = get_settings()

View File

@ -7,7 +7,7 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from slowapi import ( from slowapi import (
Limiter, Limiter,
_rate_limit_exceeded_handler, _rate_limit_exceeded_handler,
) )
from slowapi.errors import RateLimitExceeded from slowapi.errors import RateLimitExceeded
@ -22,26 +22,29 @@ def create_app() -> FastAPI:
""" """
Application factory function Application factory function
""" """
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind = engine)
app = FastAPI( app = FastAPI(
title=settings.APP_NAME, title = settings.APP_NAME,
version=settings.VERSION, version = settings.VERSION,
docs_url="/api/docs", docs_url = "/api/docs",
redoc_url="/api/redoc", redoc_url = "/api/redoc",
debug=settings.DEBUG, debug = settings.DEBUG,
) )
limiter = Limiter(key_func=get_remote_address) limiter = Limiter(key_func = get_remote_address)
app.state.limiter = limiter app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_exception_handler(
RateLimitExceeded,
_rate_limit_exceeded_handler
)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=settings.cors_origins_list, allow_origins = settings.cors_origins_list,
allow_credentials=True, allow_credentials = True,
allow_methods=["*"], allow_methods = ["*"],
allow_headers=["*"], allow_headers = ["*"],
) )
_register_routes(app) _register_routes(app)
@ -49,7 +52,6 @@ def create_app() -> FastAPI:
return app return app
def _register_routes(app: FastAPI) -> None: def _register_routes(app: FastAPI) -> None:
""" """
Register all application routes Register all application routes
@ -74,4 +76,3 @@ def _register_routes(app: FastAPI) -> None:
app.include_router(auth_router) app.include_router(auth_router)
app.include_router(scans_router) app.include_router(scans_router)

View File

@ -14,7 +14,7 @@ app = create_app()
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run( uvicorn.run(
"main:app", "main:app",
host=settings.BACKEND_HOST, host = settings.BACKEND_HOST,
port=settings.BACKEND_PORT, port = settings.BACKEND_PORT,
reload=settings.DEBUG, reload = settings.DEBUG,
) )

View File

@ -5,6 +5,7 @@ API route handlers
from .auth import router as auth_router from .auth import router as auth_router
from .scans import router as scans_router from .scans import router as scans_router
__all__ = [ __all__ = [
"auth_router", "auth_router",
"scans_router", "scans_router",

View File

@ -4,9 +4,9 @@ Authentication routes - registration and login
""" """
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
Depends, Depends,
Request, Request,
status, status,
) )
from slowapi import Limiter from slowapi import Limiter
@ -24,14 +24,14 @@ from ..schemas.user_schemas import (
from ..services.auth_service import AuthService from ..services.auth_service import AuthService
router = APIRouter(prefix="/auth", tags=["authentication"]) router = APIRouter(prefix = "/auth", tags = ["authentication"])
limiter = Limiter(key_func=get_remote_address) limiter = Limiter(key_func = get_remote_address)
@router.post( @router.post(
"/register", "/register",
response_model=UserResponse, response_model = UserResponse,
status_code=status.HTTP_201_CREATED, status_code = status.HTTP_201_CREATED,
) )
@limiter.limit(settings.API_RATE_LIMIT_REGISTER) @limiter.limit(settings.API_RATE_LIMIT_REGISTER)
async def register( async def register(
@ -47,8 +47,8 @@ async def register(
@router.post( @router.post(
"/login", "/login",
response_model=TokenResponse, response_model = TokenResponse,
status_code=status.HTTP_200_OK, status_code = status.HTTP_200_OK,
) )
@limiter.limit(settings.API_RATE_LIMIT_LOGIN) @limiter.limit(settings.API_RATE_LIMIT_LOGIN)
async def login( async def login(
@ -60,4 +60,3 @@ async def login(
Authenticate user and receive JWT token Authenticate user and receive JWT token
""" """
return AuthService.login_user(db, login_data) return AuthService.login_user(db, login_data)

View File

@ -4,9 +4,9 @@ Scan routes - create, retrieve, and manage security scans
""" """
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
Depends, Depends,
Request, Request,
status, status,
) )
from slowapi import Limiter from slowapi import Limiter
@ -17,21 +17,21 @@ from ..config import settings
from ..core.database import get_db from ..core.database import get_db
from ..core.dependencies import get_current_user from ..core.dependencies import get_current_user
from ..schemas.scan_schemas import ( from ..schemas.scan_schemas import (
ScanRequest, ScanRequest,
ScanResponse, ScanResponse,
) )
from ..schemas.user_schemas import UserResponse from ..schemas.user_schemas import UserResponse
from ..services.scan_service import ScanService from ..services.scan_service import ScanService
router = APIRouter(prefix="/scans", tags=["scans"]) router = APIRouter(prefix = "/scans", tags = ["scans"])
limiter = Limiter(key_func=get_remote_address) limiter = Limiter(key_func = get_remote_address)
@router.post( @router.post(
"/", "/",
response_model=ScanResponse, response_model = ScanResponse,
status_code=status.HTTP_201_CREATED, status_code = status.HTTP_201_CREATED,
) )
@limiter.limit(settings.API_RATE_LIMIT_SCAN) @limiter.limit(settings.API_RATE_LIMIT_SCAN)
async def create_scan( async def create_scan(
@ -48,8 +48,8 @@ async def create_scan(
@router.get( @router.get(
"/", "/",
response_model=list[ScanResponse], response_model = list[ScanResponse],
status_code=status.HTTP_200_OK, status_code = status.HTTP_200_OK,
) )
@limiter.limit(settings.API_RATE_LIMIT_DEFAULT) @limiter.limit(settings.API_RATE_LIMIT_DEFAULT)
async def get_user_scans( async def get_user_scans(
@ -62,13 +62,18 @@ async def get_user_scans(
""" """
Get all scans for the authenticated user Get all scans for the authenticated user
""" """
return ScanService.get_user_scans(db, current_user.id, skip, limit) return ScanService.get_user_scans(
db,
current_user.id,
skip,
limit
)
@router.get( @router.get(
"/{scan_id}", "/{scan_id}",
response_model=ScanResponse, response_model = ScanResponse,
status_code=status.HTTP_200_OK, status_code = status.HTTP_200_OK,
) )
@limiter.limit(settings.API_RATE_LIMIT_DEFAULT) @limiter.limit(settings.API_RATE_LIMIT_DEFAULT)
async def get_scan( async def get_scan(
@ -85,7 +90,7 @@ async def get_scan(
@router.delete( @router.delete(
"/{scan_id}", "/{scan_id}",
status_code=status.HTTP_204_NO_CONTENT, status_code = status.HTTP_204_NO_CONTENT,
) )
@limiter.limit(settings.API_RATE_LIMIT_DEFAULT) @limiter.limit(settings.API_RATE_LIMIT_DEFAULT)
async def delete_scan( async def delete_scan(

View File

@ -35,7 +35,6 @@ class AuthScanner(BaseScanner):
Maps to OWASP API Security Top 10 2023: API2:2023 Maps to OWASP API Security Top 10 2023: API2:2023
""" """
def scan(self) -> TestResultCreate: def scan(self) -> TestResultCreate:
""" """
Execute authentication tests Execute authentication tests
@ -46,10 +45,10 @@ class AuthScanner(BaseScanner):
missing_auth_test = self._test_missing_authentication() missing_auth_test = self._test_missing_authentication()
if missing_auth_test["vulnerable"]: if missing_auth_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details="Endpoint accessible without authentication", details = "Endpoint accessible without authentication",
evidence=missing_auth_test, evidence = missing_auth_test,
severity=Severity.HIGH, severity = Severity.HIGH,
recommendations=[ recommendations = [
"Require authentication for all sensitive endpoints", "Require authentication for all sensitive endpoints",
"Implement proper authentication middleware", "Implement proper authentication middleware",
"Return 401 Unauthorized for missing/invalid credentials", "Return 401 Unauthorized for missing/invalid credentials",
@ -60,10 +59,11 @@ class AuthScanner(BaseScanner):
jwt_test = self._test_jwt_vulnerabilities() jwt_test = self._test_jwt_vulnerabilities()
if jwt_test["vulnerable"]: if jwt_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details=f"JWT vulnerability: {jwt_test['vulnerability_type']}", details =
evidence=jwt_test, f"JWT vulnerability: {jwt_test['vulnerability_type']}",
severity=Severity.CRITICAL, evidence = jwt_test,
recommendations=jwt_test.get( severity = Severity.CRITICAL,
recommendations = jwt_test.get(
"recommendations", "recommendations",
[ [
"Properly validate JWT signatures", "Properly validate JWT signatures",
@ -77,10 +77,10 @@ class AuthScanner(BaseScanner):
invalid_token_test = self._test_invalid_token_handling() invalid_token_test = self._test_invalid_token_handling()
if invalid_token_test["vulnerable"]: if invalid_token_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details="Invalid tokens accepted by endpoint", details = "Invalid tokens accepted by endpoint",
evidence=invalid_token_test, evidence = invalid_token_test,
severity=Severity.HIGH, severity = Severity.HIGH,
recommendations=[ recommendations = [
"Reject invalid/malformed tokens with 401 status", "Reject invalid/malformed tokens with 401 status",
"Validate token format, signature, and expiration", "Validate token format, signature, and expiration",
"Log authentication failures for monitoring", "Log authentication failures for monitoring",
@ -88,15 +88,15 @@ class AuthScanner(BaseScanner):
) )
return TestResultCreate( return TestResultCreate(
test_name=TestType.AUTH, test_name = TestType.AUTH,
status=ScanStatus.SAFE, status = ScanStatus.SAFE,
severity=Severity.INFO, severity = Severity.INFO,
details="Authentication properly implemented", details = "Authentication properly implemented",
evidence_json={ evidence_json = {
"missing_auth_test": missing_auth_test, "missing_auth_test": missing_auth_test,
"invalid_token_test": invalid_token_test, "invalid_token_test": invalid_token_test,
}, },
recommendations_json=[ recommendations_json = [
"Authentication is properly configured", "Authentication is properly configured",
"Consider implementing additional security measures (2FA, refresh tokens)", "Consider implementing additional security measures (2FA, refresh tokens)",
], ],
@ -114,7 +114,8 @@ class AuthScanner(BaseScanner):
session_without_auth = self.session.__class__() session_without_auth = self.session.__class__()
session_without_auth.headers.update( session_without_auth.headers.update(
{ {
"User-Agent": f"{settings.APP_NAME}/{settings.VERSION}", "User-Agent":
f"{settings.APP_NAME}/{settings.VERSION}",
"Accept": "application/json", "Accept": "application/json",
} }
) )
@ -122,22 +123,29 @@ class AuthScanner(BaseScanner):
try: try:
response = session_without_auth.get( response = session_without_auth.get(
self.target_url, self.target_url,
timeout=settings.SCANNER_CONNECTION_TIMEOUT, timeout = settings.SCANNER_CONNECTION_TIMEOUT,
) )
if response.status_code == 200: if response.status_code == 200:
return { return {
"vulnerable": True, "vulnerable":
"status_code": response.status_code, True,
"response_length": len(response.text), "status_code":
"description": "Endpoint accessible without authentication", response.status_code,
"response_length":
len(response.text),
"description":
"Endpoint accessible without authentication",
} }
if response.status_code in (401, 403): if response.status_code in (401, 403):
return { return {
"vulnerable": False, "vulnerable":
"status_code": response.status_code, False,
"description": "Endpoint properly requires authentication", "status_code":
response.status_code,
"description":
"Endpoint properly requires authentication",
} }
return { return {
@ -150,7 +158,8 @@ class AuthScanner(BaseScanner):
return { return {
"vulnerable": False, "vulnerable": False,
"error": str(e), "error": str(e),
"description": "Error testing authentication requirement", "description":
"Error testing authentication requirement",
} }
def _test_jwt_vulnerabilities(self) -> dict[str, Any]: def _test_jwt_vulnerabilities(self) -> dict[str, Any]:
@ -181,7 +190,9 @@ class AuthScanner(BaseScanner):
return { return {
"vulnerable": False, "vulnerable": False,
"tests_performed": ["none_algorithm", "signature_removal"], "tests_performed":
["none_algorithm",
"signature_removal"],
"description": "No JWT vulnerabilities detected", "description": "No JWT vulnerabilities detected",
} }
@ -201,7 +212,10 @@ class AuthScanner(BaseScanner):
for variant in none_variants: for variant in none_variants:
malicious_header = self._base64url_encode( malicious_header = self._base64url_encode(
json.dumps({"alg": variant, "typ": "JWT"}) json.dumps({
"alg": variant,
"typ": "JWT"
})
) )
malicious_token = f"{malicious_header}.{payload}." malicious_token = f"{malicious_header}.{payload}."
@ -209,15 +223,21 @@ class AuthScanner(BaseScanner):
response = self.make_request( response = self.make_request(
"GET", "GET",
"/", "/",
headers={"Authorization": f"Bearer {malicious_token}"}, headers = {
"Authorization": f"Bearer {malicious_token}"
},
) )
if response.status_code == 200: if response.status_code == 200:
return { return {
"vulnerable": True, "vulnerable":
"vulnerability_type": "JWT None Algorithm", True,
"algorithm_variant": variant, "vulnerability_type":
"status_code": response.status_code, "JWT None Algorithm",
"algorithm_variant":
variant,
"status_code":
response.status_code,
"recommendations": [ "recommendations": [
"Reject tokens with 'none' algorithm (all case variations)", "Reject tokens with 'none' algorithm (all case variations)",
"Explicitly verify signature before accepting tokens", "Explicitly verify signature before accepting tokens",
@ -252,14 +272,19 @@ class AuthScanner(BaseScanner):
response = self.make_request( response = self.make_request(
"GET", "GET",
"/", "/",
headers={"Authorization": f"Bearer {malicious_token}"}, headers = {
"Authorization": f"Bearer {malicious_token}"
},
) )
if response.status_code == 200: if response.status_code == 200:
return { return {
"vulnerable": True, "vulnerable":
"vulnerability_type": "JWT Signature Not Verified", True,
"status_code": response.status_code, "vulnerability_type":
"JWT Signature Not Verified",
"status_code":
response.status_code,
"recommendations": [ "recommendations": [
"Require valid signature on all JWT tokens", "Require valid signature on all JWT tokens",
"Reject tokens with missing or invalid signatures", "Reject tokens with missing or invalid signatures",
@ -295,13 +320,15 @@ class AuthScanner(BaseScanner):
response = self.make_request( response = self.make_request(
"GET", "GET",
"/", "/",
headers={"Authorization": f"Bearer {invalid_token}"}, headers = {
"Authorization": f"Bearer {invalid_token}"
},
) )
if response.status_code == 200: if response.status_code == 200:
accepted_invalid.append( accepted_invalid.append(
{ {
"token": invalid_token[:50], "token": invalid_token[: 50],
"status_code": response.status_code, "status_code": response.status_code,
} }
) )
@ -355,7 +382,8 @@ class AuthScanner(BaseScanner):
def _create_vulnerable_result( def _create_vulnerable_result(
self, self,
details: str, details: str,
evidence: dict[str, Any], evidence: dict[str,
Any],
severity: Severity = Severity.HIGH, severity: Severity = Severity.HIGH,
recommendations: list[str] | None = None, recommendations: list[str] | None = None,
) -> TestResultCreate: ) -> TestResultCreate:
@ -372,10 +400,10 @@ class AuthScanner(BaseScanner):
TestResultCreate: Vulnerable result TestResultCreate: Vulnerable result
""" """
return TestResultCreate( return TestResultCreate(
test_name=TestType.AUTH, test_name = TestType.AUTH,
status=ScanStatus.VULNERABLE, status = ScanStatus.VULNERABLE,
severity=severity, severity = severity,
details=details, details = details,
evidence_json=evidence, evidence_json = evidence,
recommendations_json=recommendations or [], recommendations_json = recommendations or [],
) )

View File

@ -87,7 +87,8 @@ class BaseScanner(ABC):
jitter_ms = settings.DEFAULT_JITTER_MS jitter_ms = settings.DEFAULT_JITTER_MS
required_delay = 1.0 / ( required_delay = 1.0 / (
self.max_requests / settings.SCANNER_RATE_LIMIT_WINDOW_SECONDS self.max_requests /
settings.SCANNER_RATE_LIMIT_WINDOW_SECONDS
) )
jitter = random.uniform(0, jitter_ms / 1000.0) jitter = random.uniform(0, jitter_ms / 1000.0)
@ -138,7 +139,11 @@ class BaseScanner(ABC):
try: try:
start_time = time.time() start_time = time.time()
response = self.session.request(method, url, **kwargs) response = self.session.request(method, url, **kwargs)
setattr(response, "request_time", time.time() - start_time) setattr(
response,
"request_time",
time.time() - start_time
)
self.request_count += 1 self.request_count += 1

View File

@ -30,7 +30,6 @@ class IDORScanner(BaseScanner):
Maps to OWASP API Security Top 10 2023: API1:2023 Maps to OWASP API Security Top 10 2023: API1:2023
""" """
def scan(self) -> TestResultCreate: def scan(self) -> TestResultCreate:
""" """
Execute IDOR/BOLA tests Execute IDOR/BOLA tests
@ -42,10 +41,11 @@ class IDORScanner(BaseScanner):
if id_enumeration_test["vulnerable"]: if id_enumeration_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details=f"IDOR vulnerability detected: {id_enumeration_test['vulnerability_type']}", details =
evidence=id_enumeration_test, f"IDOR vulnerability detected: {id_enumeration_test['vulnerability_type']}",
severity=Severity.HIGH, evidence = id_enumeration_test,
recommendations=[ severity = Severity.HIGH,
recommendations = [
"Implement proper authorization checks for all object access", "Implement proper authorization checks for all object access",
"Verify user owns/has permission to access requested resource", "Verify user owns/has permission to access requested resource",
"Use UUIDs instead of sequential IDs (but still check authorization)", "Use UUIDs instead of sequential IDs (but still check authorization)",
@ -58,10 +58,11 @@ class IDORScanner(BaseScanner):
if predictable_id_test["vulnerable"]: if predictable_id_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details="Predictable ID patterns detected enabling enumeration", details =
evidence=predictable_id_test, "Predictable ID patterns detected enabling enumeration",
severity=Severity.MEDIUM, evidence = predictable_id_test,
recommendations=[ severity = Severity.MEDIUM,
recommendations = [
"Use non-sequential, non-predictable identifiers (UUIDs)", "Use non-sequential, non-predictable identifiers (UUIDs)",
"Implement rate limiting on ID-based endpoints", "Implement rate limiting on ID-based endpoints",
"Add authorization checks regardless of ID format", "Add authorization checks regardless of ID format",
@ -69,15 +70,15 @@ class IDORScanner(BaseScanner):
) )
return TestResultCreate( return TestResultCreate(
test_name=TestType.IDOR, test_name = TestType.IDOR,
status=ScanStatus.SAFE, status = ScanStatus.SAFE,
severity=Severity.INFO, severity = Severity.INFO,
details="No IDOR/BOLA vulnerabilities detected", details = "No IDOR/BOLA vulnerabilities detected",
evidence_json={ evidence_json = {
"id_enumeration_test": id_enumeration_test, "id_enumeration_test": id_enumeration_test,
"predictable_id_test": predictable_id_test, "predictable_id_test": predictable_id_test,
}, },
recommendations_json=[ recommendations_json = [
"Authorization checks appear to be in place", "Authorization checks appear to be in place",
"Continue monitoring for authorization bypasses", "Continue monitoring for authorization bypasses",
], ],
@ -101,7 +102,9 @@ class IDORScanner(BaseScanner):
"description": "No IDs found in endpoint responses", "description": "No IDs found in endpoint responses",
} }
numeric_test = self._test_numeric_id_manipulation(extracted_ids) numeric_test = self._test_numeric_id_manipulation(
extracted_ids
)
if numeric_test["vulnerable"]: if numeric_test["vulnerable"]:
return numeric_test return numeric_test
@ -133,23 +136,31 @@ class IDORScanner(BaseScanner):
response_text = response.text response_text = response.text
uuid_pattern = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' uuid_pattern = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
uuids = re.findall(uuid_pattern, response_text, re.IGNORECASE) uuids = re.findall(
uuid_pattern,
response_text,
re.IGNORECASE
)
numeric_id_pattern = r'"id"\s*:\s*(\d+)' numeric_id_pattern = r'"id"\s*:\s*(\d+)'
numeric_ids = re.findall(numeric_id_pattern, response_text) numeric_ids = re.findall(
numeric_id_pattern,
response_text
)
ids = [] ids = []
ids.extend(uuids[:3]) ids.extend(uuids[: 3])
ids.extend([int(nid) for nid in numeric_ids[:3]]) ids.extend([int(nid) for nid in numeric_ids[: 3]])
return ids return ids
except Exception: except Exception:
return [] return []
def _test_numeric_id_manipulation( def _test_numeric_id_manipulation(self,
self, extracted_ids: list[Any] extracted_ids: list[Any]
) -> dict[str, Any]: ) -> dict[str,
Any]:
""" """
Test numeric ID manipulation for IDOR Test numeric ID manipulation for IDOR
@ -160,7 +171,8 @@ class IDORScanner(BaseScanner):
dict[str, Any]: Numeric ID manipulation test results dict[str, Any]: Numeric ID manipulation test results
""" """
numeric_ids = [ numeric_ids = [
id_val for id_val in extracted_ids if isinstance(id_val, int) id_val for id_val in extracted_ids
if isinstance(id_val, int)
] ]
if not numeric_ids: if not numeric_ids:
@ -208,9 +220,10 @@ class IDORScanner(BaseScanner):
"numeric_ids_tested": len(test_ids), "numeric_ids_tested": len(test_ids),
} }
def _test_string_id_manipulation( def _test_string_id_manipulation(self,
self, extracted_ids: list[Any] extracted_ids: list[Any]
) -> dict[str, Any]: ) -> dict[str,
Any]:
""" """
Test string/UUID ID manipulation for IDOR Test string/UUID ID manipulation for IDOR
@ -221,7 +234,8 @@ class IDORScanner(BaseScanner):
dict[str, Any]: String ID manipulation test results dict[str, Any]: String ID manipulation test results
""" """
string_ids = [ string_ids = [
id_val for id_val in extracted_ids if isinstance(id_val, str) id_val for id_val in extracted_ids
if isinstance(id_val, str)
] ]
if not string_ids: if not string_ids:
@ -291,7 +305,7 @@ class IDORScanner(BaseScanner):
"vulnerable": True, "vulnerable": True,
"pattern_type": "Sequential IDs", "pattern_type": "Sequential IDs",
"id_difference": diff1, "id_difference": diff1,
"example_ids": numeric_ids1[:3], "example_ids": numeric_ids1[: 3],
} }
return { return {
@ -309,7 +323,8 @@ class IDORScanner(BaseScanner):
def _create_vulnerable_result( def _create_vulnerable_result(
self, self,
details: str, details: str,
evidence: dict[str, Any], evidence: dict[str,
Any],
severity: Severity = Severity.HIGH, severity: Severity = Severity.HIGH,
recommendations: list[str] | None = None, recommendations: list[str] | None = None,
) -> TestResultCreate: ) -> TestResultCreate:
@ -326,10 +341,10 @@ class IDORScanner(BaseScanner):
TestResultCreate: Vulnerable result TestResultCreate: Vulnerable result
""" """
return TestResultCreate( return TestResultCreate(
test_name=TestType.IDOR, test_name = TestType.IDOR,
status=ScanStatus.VULNERABLE, status = ScanStatus.VULNERABLE,
severity=severity, severity = severity,
details=details, details = details,
evidence_json=evidence, evidence_json = evidence,
recommendations_json=recommendations or [], recommendations_json = recommendations or [],
) )

View File

@ -277,9 +277,12 @@ class RateLimitBypassPayloads:
""" """
HEADER_PATTERNS = { HEADER_PATTERNS = {
"limit": r"x-ratelimit-limit|x-rate-limit-limit|ratelimit-limit", "limit":
"remaining": r"x-ratelimit-remaining|x-rate-limit-remaining|ratelimit-remaining", r"x-ratelimit-limit|x-rate-limit-limit|ratelimit-limit",
"reset": r"x-ratelimit-reset|x-rate-limit-reset|ratelimit-reset", "remaining":
r"x-ratelimit-remaining|x-rate-limit-remaining|ratelimit-remaining",
"reset":
r"x-ratelimit-reset|x-rate-limit-reset|ratelimit-reset",
"retry_after": r"retry-after", "retry_after": r"retry-after",
} }

View File

@ -110,7 +110,8 @@ class RateLimitScanner(BaseScanner):
Returns: Returns:
dict[str, Any]: Rate limiting detection results dict[str, Any]: Rate limiting detection results
""" """
rate_limit_patterns = RateLimitBypassPayloads.get_header_patterns() rate_limit_patterns = RateLimitBypassPayloads.get_header_patterns(
)
results = { results = {
"rate_limit_detected": False, "rate_limit_detected": False,

View File

@ -31,7 +31,6 @@ class SQLiScanner(BaseScanner):
Uses payloads covering MySQL, PostgreSQL, MSSQL, Oracle Uses payloads covering MySQL, PostgreSQL, MSSQL, Oracle
""" """
def scan(self) -> TestResultCreate: def scan(self) -> TestResultCreate:
""" """
Execute SQL injection tests Execute SQL injection tests
@ -42,10 +41,11 @@ class SQLiScanner(BaseScanner):
error_based_test = self._test_error_based_sqli() error_based_test = self._test_error_based_sqli()
if error_based_test["vulnerable"]: if error_based_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details=f"Error-based SQL injection detected: {error_based_test['database_type']}", details =
evidence=error_based_test, f"Error-based SQL injection detected: {error_based_test['database_type']}",
severity=Severity.CRITICAL, evidence = error_based_test,
recommendations=[ severity = Severity.CRITICAL,
recommendations = [
"Use parameterized queries (prepared statements)", "Use parameterized queries (prepared statements)",
"Never concatenate user input into SQL queries", "Never concatenate user input into SQL queries",
"Implement input validation and sanitization", "Implement input validation and sanitization",
@ -57,10 +57,10 @@ class SQLiScanner(BaseScanner):
boolean_based_test = self._test_boolean_based_sqli() boolean_based_test = self._test_boolean_based_sqli()
if boolean_based_test["vulnerable"]: if boolean_based_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details="Boolean-based blind SQL injection detected", details = "Boolean-based blind SQL injection detected",
evidence=boolean_based_test, evidence = boolean_based_test,
severity=Severity.CRITICAL, severity = Severity.CRITICAL,
recommendations=[ recommendations = [
"Use parameterized queries for all database operations", "Use parameterized queries for all database operations",
"Implement proper input validation", "Implement proper input validation",
"Avoid exposing different responses for true/false conditions", "Avoid exposing different responses for true/false conditions",
@ -70,10 +70,11 @@ class SQLiScanner(BaseScanner):
time_based_test = self._test_time_based_sqli() time_based_test = self._test_time_based_sqli()
if time_based_test["vulnerable"]: if time_based_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details=f"Time-based blind SQL injection detected: {time_based_test['database_type']}", details =
evidence=time_based_test, f"Time-based blind SQL injection detected: {time_based_test['database_type']}",
severity=Severity.CRITICAL, evidence = time_based_test,
recommendations=[ severity = Severity.CRITICAL,
recommendations = [
"Use parameterized queries exclusively", "Use parameterized queries exclusively",
"Implement strict input validation", "Implement strict input validation",
"Monitor for unusual response time patterns", "Monitor for unusual response time patterns",
@ -81,16 +82,16 @@ class SQLiScanner(BaseScanner):
) )
return TestResultCreate( return TestResultCreate(
test_name=TestType.SQLI, test_name = TestType.SQLI,
status=ScanStatus.SAFE, status = ScanStatus.SAFE,
severity=Severity.INFO, severity = Severity.INFO,
details="No SQL injection vulnerabilities detected", details = "No SQL injection vulnerabilities detected",
evidence_json={ evidence_json = {
"error_based_test": error_based_test, "error_based_test": error_based_test,
"boolean_based_test": boolean_based_test, "boolean_based_test": boolean_based_test,
"time_based_test": time_based_test, "time_based_test": time_based_test,
}, },
recommendations_json=[ recommendations_json = [
"Continue using parameterized queries", "Continue using parameterized queries",
"Regularly update security testing", "Regularly update security testing",
], ],
@ -111,9 +112,7 @@ class SQLiScanner(BaseScanner):
for payload in basic_payloads: for payload in basic_payloads:
try: try:
response = self.make_request( response = self.make_request("GET", f"/?id={payload}")
"GET", f"/?id={payload}"
)
response_text_lower = response.text.lower() response_text_lower = response.text.lower()
@ -126,7 +125,8 @@ class SQLiScanner(BaseScanner):
"payload": payload, "payload": payload,
"status_code": response.status_code, "status_code": response.status_code,
"error_signature": signature, "error_signature": signature,
"response_excerpt": response.text[:500], "response_excerpt":
response.text[: 500],
} }
except Exception: except Exception:
@ -160,8 +160,14 @@ class SQLiScanner(BaseScanner):
} }
boolean_payloads = SQLiPayloads.BOOLEAN_BASED_BLIND boolean_payloads = SQLiPayloads.BOOLEAN_BASED_BLIND
true_payloads = [p for p in boolean_payloads if "AND '1'='1" in p or "AND 1=1" in p] true_payloads = [
false_payloads = [p for p in boolean_payloads if "AND '1'='2" in p or "AND 1=2" in p or "AND 1=0" in p] p for p in boolean_payloads
if "AND '1'='1" in p or "AND 1=1" in p
]
false_payloads = [
p for p in boolean_payloads if "AND '1'='2" in p
or "AND 1=2" in p or "AND 1=0" in p
]
true_lengths = [] true_lengths = []
for payload in true_payloads: for payload in true_payloads:
@ -180,14 +186,18 @@ class SQLiScanner(BaseScanner):
if length_diff > 100 and avg_true != avg_false: if length_diff > 100 and avg_true != avg_false:
return { return {
"vulnerable": True, "vulnerable":
"baseline_length": baseline_length, True,
"true_condition_avg_length": avg_true, "baseline_length":
"false_condition_avg_length": avg_false, baseline_length,
"length_difference": length_diff, "true_condition_avg_length":
"confidence": "HIGH" avg_true,
if length_diff > 500 "false_condition_avg_length":
else "MEDIUM", avg_false,
"length_difference":
length_diff,
"confidence":
"HIGH" if length_diff > 500 else "MEDIUM",
} }
return { return {
@ -203,9 +213,9 @@ class SQLiScanner(BaseScanner):
"description": "Error testing boolean-based SQLi", "description": "Error testing boolean-based SQLi",
} }
def _test_time_based_sqli( def _test_time_based_sqli(self,
self, delay_seconds: int = 5 delay_seconds: int = 5) -> dict[str,
) -> dict[str, Any]: Any]:
""" """
Test for time based blind SQL injection Test for time based blind SQL injection
@ -227,9 +237,14 @@ class SQLiScanner(BaseScanner):
all_time_payloads = SQLiPayloads.TIME_BASED_BLIND all_time_payloads = SQLiPayloads.TIME_BASED_BLIND
delay_payloads = { delay_payloads = {
"mysql": [p for p in all_time_payloads if "SLEEP" in p], "mysql":
"postgres": [p for p in all_time_payloads if "pg_sleep" in p], [p for p in all_time_payloads if "SLEEP" in p],
"mssql": [p for p in all_time_payloads if "WAITFOR" in p], "postgres": [
p for p in all_time_payloads if "pg_sleep" in p
],
"mssql": [
p for p in all_time_payloads if "WAITFOR" in p
],
} }
for db_type, payloads in delay_payloads.items(): for db_type, payloads in delay_payloads.items():
@ -241,9 +256,13 @@ class SQLiScanner(BaseScanner):
response = self.make_request( response = self.make_request(
"GET", "GET",
f"/?id={payload}", f"/?id={payload}",
timeout=delay_seconds + 10, timeout = delay_seconds + 10,
)
elapsed = getattr(
response,
"request_time",
0.0
) )
elapsed = getattr(response, "request_time", 0.0)
delay_times.append(elapsed) delay_times.append(elapsed)
except Exception: except Exception:
@ -255,22 +274,27 @@ class SQLiScanner(BaseScanner):
if avg_delay >= expected_delay_time - 1: if avg_delay >= expected_delay_time - 1:
confidence = ( confidence = (
"HIGH" "HIGH" if avg_delay >= expected_delay_time
if avg_delay >= expected_delay_time
else "MEDIUM" else "MEDIUM"
) )
return { return {
"vulnerable": True, "vulnerable":
"database_type": db_type, True,
"payload": payload, "database_type":
"baseline_time": f"{baseline_mean:.3f}s", db_type,
"response_time": f"{avg_delay:.3f}s", "payload":
"expected_delay": f"{expected_delay_time:.3f}s", payload,
"confidence": confidence, "baseline_time":
"individual_times": [ f"{baseline_mean:.3f}s",
f"{t:.3f}s" for t in delay_times "response_time":
], f"{avg_delay:.3f}s",
"expected_delay":
f"{expected_delay_time:.3f}s",
"confidence":
confidence,
"individual_times":
[f"{t:.3f}s" for t in delay_times],
} }
return { return {
@ -290,7 +314,8 @@ class SQLiScanner(BaseScanner):
def _create_vulnerable_result( def _create_vulnerable_result(
self, self,
details: str, details: str,
evidence: dict[str, Any], evidence: dict[str,
Any],
severity: Severity = Severity.CRITICAL, severity: Severity = Severity.CRITICAL,
recommendations: list[str] | None = None, recommendations: list[str] | None = None,
) -> TestResultCreate: ) -> TestResultCreate:
@ -307,10 +332,10 @@ class SQLiScanner(BaseScanner):
TestResultCreate: Vulnerable result TestResultCreate: Vulnerable result
""" """
return TestResultCreate( return TestResultCreate(
test_name=TestType.SQLI, test_name = TestType.SQLI,
status=ScanStatus.VULNERABLE, status = ScanStatus.VULNERABLE,
severity=severity, severity = severity,
details=details, details = details,
evidence_json=evidence, evidence_json = evidence,
recommendations_json=recommendations or [], recommendations_json = recommendations or [],
) )