mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-07-06 11:27:52 +02:00
v3.3.0 GUI dashboard + reports + model expansion + root fix
Engine:
- Fix: inject IS_SANDBOX=1 so Claude Code's --dangerously-skip-permissions
works under root (real backend runs were exiting rc=1 immediately)
- models: expand to 40 models / 13 providers, tagged CLI vs API
(NVIDIA NIM, DeepSeek, Mistral, Qwen/DashScope, Groq, Together, OpenRouter,
Ollama, Gemini) — Qwen/DeepSeek/Llama usable via API
- backends: on_start callback surfaces the exact argv ("what runs behind it")
- orchestrator: require a Playwright screenshot per confirmed finding; collect
results/activity.json; auto-generate reports after a run
- report.py: HTML always + PDF via Typst engine (.typ source emitted too)
Web dashboard (webgui/, stdlib only — no npm/build):
- Sidebar dashboard (PentAGI-style): Run / Agents / Insights / Reports / Settings
- Multi-target runs; live execution console + per-task activity; finding cards
with screenshots; backend+provider+model pickers (CLI & API)
- Agents tab: browse 213 + add new .md agents from the UI
- Insights: interactive RL-weight + severity charts
- Reports: download/preview PDF + HTML
- Settings/API: execution mode, per-provider API keys, orchestrator, verbosity
- Endpoints: /api/agents (GET/POST), /api/rl, /api/config, /api/reports,
/reports/* + /shots/* static serving
Cleanup: retire replaced web stack (frontend React, FastAPI backend, core
orchestration, old test) to legacy/. Active engine + GUI are fully standalone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
from backend.core.vuln_engine.registry import VulnerabilityRegistry
|
||||
from backend.core.vuln_engine.payload_generator import PayloadGenerator
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
"""Lazy import for DynamicVulnerabilityEngine (requires database models)"""
|
||||
if name == "DynamicVulnerabilityEngine":
|
||||
from backend.core.vuln_engine.engine import DynamicVulnerabilityEngine
|
||||
return DynamicVulnerabilityEngine
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
__all__ = ["DynamicVulnerabilityEngine", "VulnerabilityRegistry", "PayloadGenerator"]
|
||||
+2318
File diff suppressed because it is too large
Load Diff
+287
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
NeuroSploit v3 - Dynamic Vulnerability Engine
|
||||
|
||||
The core of NeuroSploit v3: prompt-driven vulnerability testing.
|
||||
Instead of hardcoded tests, this engine dynamically tests based on
|
||||
what vulnerabilities are extracted from the user's prompt.
|
||||
"""
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from typing import List, Dict, Optional, Any
|
||||
from datetime import datetime
|
||||
|
||||
from backend.core.vuln_engine.registry import VulnerabilityRegistry
|
||||
from backend.core.vuln_engine.payload_generator import PayloadGenerator
|
||||
from backend.models import Endpoint, Vulnerability, VulnerabilityTest
|
||||
from backend.schemas.prompt import VulnerabilityTypeExtracted
|
||||
|
||||
|
||||
class TestResult:
|
||||
"""Result of a vulnerability test"""
|
||||
def __init__(
|
||||
self,
|
||||
vuln_type: str,
|
||||
is_vulnerable: bool,
|
||||
confidence: float,
|
||||
payload: str,
|
||||
request_data: dict,
|
||||
response_data: dict,
|
||||
evidence: Optional[str] = None
|
||||
):
|
||||
self.vuln_type = vuln_type
|
||||
self.is_vulnerable = is_vulnerable
|
||||
self.confidence = confidence
|
||||
self.payload = payload
|
||||
self.request_data = request_data
|
||||
self.response_data = response_data
|
||||
self.evidence = evidence
|
||||
|
||||
|
||||
class DynamicVulnerabilityEngine:
|
||||
"""
|
||||
Prompt-driven vulnerability testing engine.
|
||||
|
||||
Key principles:
|
||||
1. Tests ONLY what the prompt specifies
|
||||
2. Generates payloads dynamically based on context
|
||||
3. Uses multiple detection techniques per vulnerability type
|
||||
4. Adapts based on target responses
|
||||
"""
|
||||
|
||||
def __init__(self, llm_manager=None):
|
||||
self.llm_manager = llm_manager
|
||||
self.registry = VulnerabilityRegistry()
|
||||
self.payload_generator = PayloadGenerator()
|
||||
self.session: Optional[aiohttp.ClientSession] = None
|
||||
self.timeout = aiohttp.ClientTimeout(total=30)
|
||||
|
||||
async def __aenter__(self):
|
||||
self.session = aiohttp.ClientSession(timeout=self.timeout)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
|
||||
async def test_endpoint(
|
||||
self,
|
||||
endpoint: Endpoint,
|
||||
vuln_types: List[VulnerabilityTypeExtracted],
|
||||
context: Dict[str, Any],
|
||||
progress_callback=None
|
||||
) -> List[TestResult]:
|
||||
"""
|
||||
Test an endpoint for specified vulnerability types.
|
||||
|
||||
Args:
|
||||
endpoint: The endpoint to test
|
||||
vuln_types: List of vulnerability types to test for
|
||||
context: Additional context (technologies, WAF info, etc.)
|
||||
progress_callback: Optional callback for progress updates
|
||||
|
||||
Returns:
|
||||
List of test results
|
||||
"""
|
||||
results = []
|
||||
|
||||
if not self.session:
|
||||
self.session = aiohttp.ClientSession(timeout=self.timeout)
|
||||
|
||||
for vuln in vuln_types:
|
||||
try:
|
||||
if progress_callback:
|
||||
await progress_callback(f"Testing {vuln.type} on {endpoint.url}")
|
||||
|
||||
# Get tester for this vulnerability type
|
||||
tester = self.registry.get_tester(vuln.type)
|
||||
|
||||
# Get payloads for this vulnerability and endpoint
|
||||
payloads = await self.payload_generator.get_payloads(
|
||||
vuln_type=vuln.type,
|
||||
endpoint=endpoint,
|
||||
context=context
|
||||
)
|
||||
|
||||
# Test each payload
|
||||
for payload in payloads:
|
||||
result = await self._execute_test(
|
||||
endpoint=endpoint,
|
||||
vuln_type=vuln.type,
|
||||
payload=payload,
|
||||
tester=tester,
|
||||
context=context
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
# If vulnerable, try to get more evidence
|
||||
if result.is_vulnerable:
|
||||
deeper_results = await self._deep_test(
|
||||
endpoint=endpoint,
|
||||
vuln_type=vuln.type,
|
||||
initial_result=result,
|
||||
tester=tester,
|
||||
context=context
|
||||
)
|
||||
results.extend(deeper_results)
|
||||
break # Found vulnerability, move to next type
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error testing {vuln.type}: {e}")
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
async def _execute_test(
|
||||
self,
|
||||
endpoint: Endpoint,
|
||||
vuln_type: str,
|
||||
payload: str,
|
||||
tester,
|
||||
context: Dict
|
||||
) -> TestResult:
|
||||
"""Execute a single vulnerability test"""
|
||||
request_data = {
|
||||
"url": endpoint.url,
|
||||
"method": endpoint.method,
|
||||
"payload": payload,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
try:
|
||||
# Build the test request
|
||||
test_url, test_params, test_headers, test_body = tester.build_request(
|
||||
endpoint=endpoint,
|
||||
payload=payload
|
||||
)
|
||||
|
||||
# Send the request
|
||||
async with self.session.request(
|
||||
method=endpoint.method,
|
||||
url=test_url,
|
||||
params=test_params,
|
||||
headers=test_headers,
|
||||
data=test_body,
|
||||
ssl=False,
|
||||
allow_redirects=False
|
||||
) as response:
|
||||
response_text = await response.text()
|
||||
response_data = {
|
||||
"status": response.status,
|
||||
"headers": dict(response.headers),
|
||||
"body_preview": response_text[:2000] if response_text else "",
|
||||
"content_length": len(response_text) if response_text else 0
|
||||
}
|
||||
|
||||
# Analyze response for vulnerability
|
||||
is_vulnerable, confidence, evidence = tester.analyze_response(
|
||||
payload=payload,
|
||||
response_status=response.status,
|
||||
response_headers=dict(response.headers),
|
||||
response_body=response_text,
|
||||
context=context
|
||||
)
|
||||
|
||||
return TestResult(
|
||||
vuln_type=vuln_type,
|
||||
is_vulnerable=is_vulnerable,
|
||||
confidence=confidence,
|
||||
payload=payload,
|
||||
request_data=request_data,
|
||||
response_data=response_data,
|
||||
evidence=evidence
|
||||
)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
# Timeout might indicate time-based injection
|
||||
response_data = {"error": "timeout", "timeout_seconds": self.timeout.total}
|
||||
is_vulnerable = tester.check_timeout_vulnerability(vuln_type)
|
||||
return TestResult(
|
||||
vuln_type=vuln_type,
|
||||
is_vulnerable=is_vulnerable,
|
||||
confidence=0.7 if is_vulnerable else 0.0,
|
||||
payload=payload,
|
||||
request_data=request_data,
|
||||
response_data=response_data,
|
||||
evidence="Request timed out - possible time-based vulnerability" if is_vulnerable else None
|
||||
)
|
||||
except Exception as e:
|
||||
response_data = {"error": str(e)}
|
||||
return TestResult(
|
||||
vuln_type=vuln_type,
|
||||
is_vulnerable=False,
|
||||
confidence=0.0,
|
||||
payload=payload,
|
||||
request_data=request_data,
|
||||
response_data=response_data,
|
||||
evidence=None
|
||||
)
|
||||
|
||||
async def _deep_test(
|
||||
self,
|
||||
endpoint: Endpoint,
|
||||
vuln_type: str,
|
||||
initial_result: TestResult,
|
||||
tester,
|
||||
context: Dict
|
||||
) -> List[TestResult]:
|
||||
"""
|
||||
Perform deeper testing after initial vulnerability confirmation.
|
||||
This helps establish higher confidence and better PoC.
|
||||
"""
|
||||
results = []
|
||||
|
||||
# Get exploitation payloads
|
||||
deeper_payloads = await self.payload_generator.get_exploitation_payloads(
|
||||
vuln_type=vuln_type,
|
||||
initial_payload=initial_result.payload,
|
||||
context=context
|
||||
)
|
||||
|
||||
for payload in deeper_payloads[:3]: # Limit to 3 deeper tests
|
||||
result = await self._execute_test(
|
||||
endpoint=endpoint,
|
||||
vuln_type=vuln_type,
|
||||
payload=payload,
|
||||
tester=tester,
|
||||
context=context
|
||||
)
|
||||
if result.is_vulnerable:
|
||||
result.confidence = min(result.confidence + 0.1, 1.0)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
async def create_vulnerability_record(
|
||||
self,
|
||||
scan_id: str,
|
||||
endpoint: Endpoint,
|
||||
result: TestResult
|
||||
) -> Vulnerability:
|
||||
"""Create a vulnerability record from a test result"""
|
||||
# Get severity based on vulnerability type
|
||||
severity = self.registry.get_severity(result.vuln_type)
|
||||
|
||||
# Get CWE ID
|
||||
cwe_id = self.registry.get_cwe_id(result.vuln_type)
|
||||
|
||||
# Get remediation advice
|
||||
remediation = self.registry.get_remediation(result.vuln_type)
|
||||
|
||||
# Generate title
|
||||
title = self.registry.get_title(result.vuln_type)
|
||||
|
||||
return Vulnerability(
|
||||
scan_id=scan_id,
|
||||
title=f"{title} on {endpoint.path or endpoint.url}",
|
||||
vulnerability_type=result.vuln_type,
|
||||
severity=severity,
|
||||
cwe_id=cwe_id,
|
||||
description=self.registry.get_description(result.vuln_type),
|
||||
affected_endpoint=endpoint.url,
|
||||
poc_request=str(result.request_data),
|
||||
poc_response=str(result.response_data.get("body_preview", ""))[:5000],
|
||||
poc_payload=result.payload,
|
||||
impact=self.registry.get_impact(result.vuln_type),
|
||||
remediation=remediation,
|
||||
ai_analysis=result.evidence
|
||||
)
|
||||
+1040
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+616
@@ -0,0 +1,616 @@
|
||||
"""
|
||||
NeuroSploit v3 - Vulnerability Registry
|
||||
|
||||
Registry of all vulnerability types and their testers.
|
||||
Provides metadata, severity info, and tester classes.
|
||||
"""
|
||||
from typing import Dict, Optional, Tuple
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
from backend.core.vuln_engine.testers.injection import (
|
||||
XSSReflectedTester, XSSStoredTester, XSSDomTester,
|
||||
SQLiErrorTester, SQLiUnionTester, SQLiBlindTester, SQLiTimeTester,
|
||||
CommandInjectionTester, SSTITester, NoSQLInjectionTester
|
||||
)
|
||||
from backend.core.vuln_engine.testers.advanced_injection import (
|
||||
LdapInjectionTester, XpathInjectionTester, GraphqlInjectionTester,
|
||||
CrlfInjectionTester, HeaderInjectionTester, EmailInjectionTester,
|
||||
ELInjectionTester, LogInjectionTester, HtmlInjectionTester,
|
||||
CsvInjectionTester, OrmInjectionTester
|
||||
)
|
||||
from backend.core.vuln_engine.testers.file_access import (
|
||||
LFITester, RFITester, PathTraversalTester, XXETester, FileUploadTester,
|
||||
ArbitraryFileReadTester, ArbitraryFileDeleteTester, ZipSlipTester
|
||||
)
|
||||
from backend.core.vuln_engine.testers.request_forgery import (
|
||||
SSRFTester, CSRFTester, GraphqlIntrospectionTester, GraphqlDosTester
|
||||
)
|
||||
from backend.core.vuln_engine.testers.auth import (
|
||||
AuthBypassTester, JWTManipulationTester, SessionFixationTester,
|
||||
WeakPasswordTester, DefaultCredentialsTester, TwoFactorBypassTester,
|
||||
OauthMisconfigTester
|
||||
)
|
||||
from backend.core.vuln_engine.testers.authorization import (
|
||||
IDORTester, BOLATester, PrivilegeEscalationTester,
|
||||
BflaTester, MassAssignmentTester, ForcedBrowsingTester
|
||||
)
|
||||
from backend.core.vuln_engine.testers.client_side import (
|
||||
CORSTester, ClickjackingTester, OpenRedirectTester,
|
||||
DomClobberingTester, PostMessageVulnTester, WebsocketHijackTester,
|
||||
PrototypePollutionTester, CssInjectionTester, TabnabbingTester
|
||||
)
|
||||
from backend.core.vuln_engine.testers.infrastructure import (
|
||||
SecurityHeadersTester, SSLTester, HTTPMethodsTester,
|
||||
DirectoryListingTester, DebugModeTester, ExposedAdminPanelTester,
|
||||
ExposedApiDocsTester, InsecureCookieFlagsTester
|
||||
)
|
||||
from backend.core.vuln_engine.testers.logic import (
|
||||
RaceConditionTester, BusinessLogicTester, RateLimitBypassTester,
|
||||
ParameterPollutionTester, TypeJugglingTester, TimingAttackTester,
|
||||
HostHeaderInjectionTester, HttpSmugglingTester, CachePoisoningTester
|
||||
)
|
||||
from backend.core.vuln_engine.testers.data_exposure import (
|
||||
SensitiveDataExposureTester, InformationDisclosureTester,
|
||||
ApiKeyExposureTester, SourceCodeDisclosureTester,
|
||||
BackupFileExposureTester, VersionDisclosureTester
|
||||
)
|
||||
from backend.core.vuln_engine.testers.cloud_supply import (
|
||||
S3BucketMisconfigTester, CloudMetadataExposureTester,
|
||||
SubdomainTakeoverTester, VulnerableDependencyTester,
|
||||
ContainerEscapeTester, ServerlessMisconfigTester
|
||||
)
|
||||
|
||||
|
||||
class VulnerabilityRegistry:
|
||||
"""
|
||||
Central registry for all vulnerability types.
|
||||
|
||||
Maps vulnerability types to:
|
||||
- Tester classes
|
||||
- Severity levels
|
||||
- CWE IDs
|
||||
- Descriptions
|
||||
- Remediation advice
|
||||
"""
|
||||
|
||||
# Vulnerability metadata
|
||||
VULNERABILITY_INFO = {
|
||||
# XSS
|
||||
"xss_reflected": {
|
||||
"title": "Reflected Cross-Site Scripting (XSS)",
|
||||
"severity": "medium",
|
||||
"cwe_id": "CWE-79",
|
||||
"description": "Reflected XSS occurs when user input is immediately returned by a web application in an error message, search result, or any other response that includes some or all of the input provided by the user as part of the request, without that data being made safe to render in the browser.",
|
||||
"impact": "An attacker can execute arbitrary JavaScript in the victim's browser, potentially stealing session cookies, capturing credentials, or performing actions on behalf of the user.",
|
||||
"remediation": "1. Encode all user input when rendering in HTML context\n2. Use Content-Security-Policy headers\n3. Set HttpOnly flag on sensitive cookies\n4. Use modern frameworks with auto-escaping"
|
||||
},
|
||||
"xss_stored": {
|
||||
"title": "Stored Cross-Site Scripting (XSS)",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-79",
|
||||
"description": "Stored XSS occurs when malicious script is permanently stored on the target server, such as in a database, message forum, visitor log, or comment field.",
|
||||
"impact": "All users who view the affected page will execute the malicious script, leading to mass credential theft, session hijacking, or malware distribution.",
|
||||
"remediation": "1. Sanitize and validate all user input before storage\n2. Encode output when rendering\n3. Implement Content-Security-Policy\n4. Use HttpOnly and Secure flags on cookies"
|
||||
},
|
||||
"xss_dom": {
|
||||
"title": "DOM-based Cross-Site Scripting",
|
||||
"severity": "medium",
|
||||
"cwe_id": "CWE-79",
|
||||
"description": "DOM-based XSS occurs when client-side JavaScript processes user input and writes it to the DOM in an unsafe way.",
|
||||
"impact": "Attacker can execute JavaScript in the user's browser through malicious links or user interaction.",
|
||||
"remediation": "1. Avoid using dangerous DOM sinks (innerHTML, eval, document.write)\n2. Use textContent instead of innerHTML\n3. Sanitize user input on the client side\n4. Implement CSP with strict policies"
|
||||
},
|
||||
|
||||
# SQL Injection
|
||||
"sqli_error": {
|
||||
"title": "Error-based SQL Injection",
|
||||
"severity": "critical",
|
||||
"cwe_id": "CWE-89",
|
||||
"description": "SQL injection vulnerability that reveals database errors containing query information, allowing attackers to extract data through error messages.",
|
||||
"impact": "Complete database compromise including data theft, modification, or deletion. May lead to remote code execution on the database server.",
|
||||
"remediation": "1. Use parameterized queries/prepared statements\n2. Implement input validation with whitelist approach\n3. Apply least privilege principle for database accounts\n4. Disable detailed error messages in production"
|
||||
},
|
||||
"sqli_union": {
|
||||
"title": "Union-based SQL Injection",
|
||||
"severity": "critical",
|
||||
"cwe_id": "CWE-89",
|
||||
"description": "SQL injection allowing UNION-based queries to extract data from other database tables.",
|
||||
"impact": "Full database extraction capability. Attacker can read all database tables, users, and potentially escalate to RCE.",
|
||||
"remediation": "1. Use parameterized queries exclusively\n2. Implement strict input validation\n3. Use stored procedures where appropriate\n4. Monitor for unusual query patterns"
|
||||
},
|
||||
"sqli_blind": {
|
||||
"title": "Blind SQL Injection (Boolean-based)",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-89",
|
||||
"description": "SQL injection where results are inferred from application behavior changes rather than direct output.",
|
||||
"impact": "Slower but complete data extraction is possible. Can lead to full database compromise.",
|
||||
"remediation": "1. Use parameterized queries\n2. Implement WAF rules for SQL injection patterns\n3. Use connection pooling with timeout limits\n4. Implement query logging and monitoring"
|
||||
},
|
||||
"sqli_time": {
|
||||
"title": "Time-based Blind SQL Injection",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-89",
|
||||
"description": "SQL injection where attacker can infer information based on time delays in responses.",
|
||||
"impact": "Complete data extraction possible, though slower. Can determine database structure and content.",
|
||||
"remediation": "1. Use parameterized queries\n2. Set strict query timeout limits\n3. Monitor for anomalously slow queries\n4. Implement rate limiting"
|
||||
},
|
||||
|
||||
# Command Injection
|
||||
"command_injection": {
|
||||
"title": "OS Command Injection",
|
||||
"severity": "critical",
|
||||
"cwe_id": "CWE-78",
|
||||
"description": "Application passes unsafe user-supplied data to a system shell, allowing execution of arbitrary OS commands.",
|
||||
"impact": "Complete system compromise. Attacker can execute any command with the application's privileges, potentially gaining full server access.",
|
||||
"remediation": "1. Avoid shell commands; use native library functions\n2. If shell required, use strict whitelist validation\n3. Never pass user input directly to shell\n4. Run with minimal privileges, use containers"
|
||||
},
|
||||
|
||||
# SSTI
|
||||
"ssti": {
|
||||
"title": "Server-Side Template Injection",
|
||||
"severity": "critical",
|
||||
"cwe_id": "CWE-94",
|
||||
"description": "User input is unsafely embedded into server-side templates, allowing template code execution.",
|
||||
"impact": "Often leads to remote code execution. Attacker can read files, execute commands, and compromise the server.",
|
||||
"remediation": "1. Never pass user input to template engines\n2. Use logic-less templates when possible\n3. Implement sandbox environments for templates\n4. Validate and sanitize all template inputs"
|
||||
},
|
||||
|
||||
# NoSQL Injection
|
||||
"nosql_injection": {
|
||||
"title": "NoSQL Injection",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-943",
|
||||
"description": "Injection attack targeting NoSQL databases like MongoDB through operator injection.",
|
||||
"impact": "Authentication bypass, data theft, and potential server compromise depending on database configuration.",
|
||||
"remediation": "1. Validate and sanitize all user input\n2. Use parameterized queries where available\n3. Disable server-side JavaScript execution\n4. Apply strict typing to query parameters"
|
||||
},
|
||||
|
||||
# File Access
|
||||
"lfi": {
|
||||
"title": "Local File Inclusion",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-98",
|
||||
"description": "Application includes local files based on user input, allowing access to sensitive files.",
|
||||
"impact": "Read sensitive configuration files, source code, and potentially achieve code execution via log poisoning.",
|
||||
"remediation": "1. Avoid dynamic file inclusion\n2. Use whitelist of allowed files\n3. Validate and sanitize file paths\n4. Implement proper access controls"
|
||||
},
|
||||
"rfi": {
|
||||
"title": "Remote File Inclusion",
|
||||
"severity": "critical",
|
||||
"cwe_id": "CWE-98",
|
||||
"description": "Application includes remote files, allowing execution of attacker-controlled code.",
|
||||
"impact": "Direct remote code execution. Complete server compromise.",
|
||||
"remediation": "1. Disable allow_url_include in PHP\n2. Use whitelists for file inclusion\n3. Never use user input in include paths\n4. Implement strict input validation"
|
||||
},
|
||||
"path_traversal": {
|
||||
"title": "Path Traversal",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-22",
|
||||
"description": "Application allows navigation outside intended directory through ../ sequences.",
|
||||
"impact": "Access to sensitive files outside web root, including configuration files and source code.",
|
||||
"remediation": "1. Validate and sanitize file paths\n2. Use basename() to strip directory components\n3. Implement chroot or containerization\n4. Use whitelist of allowed directories"
|
||||
},
|
||||
"xxe": {
|
||||
"title": "XML External Entity Injection",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-611",
|
||||
"description": "XML parser processes external entity references, allowing file access or SSRF.",
|
||||
"impact": "Read local files, perform SSRF attacks, and potentially achieve denial of service.",
|
||||
"remediation": "1. Disable external entity processing\n2. Use JSON instead of XML where possible\n3. Validate and sanitize XML input\n4. Use updated XML parsers with secure defaults"
|
||||
},
|
||||
"file_upload": {
|
||||
"title": "Arbitrary File Upload",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-434",
|
||||
"description": "Application allows uploading of dangerous file types that can be executed.",
|
||||
"impact": "Upload of web shells leading to remote code execution and complete server compromise.",
|
||||
"remediation": "1. Validate file type using magic bytes\n2. Rename uploaded files\n3. Store outside web root\n4. Disable execution in upload directory"
|
||||
},
|
||||
|
||||
# Request Forgery
|
||||
"ssrf": {
|
||||
"title": "Server-Side Request Forgery",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-918",
|
||||
"description": "Application makes requests to attacker-specified URLs, accessing internal resources.",
|
||||
"impact": "Access to internal services, cloud metadata, and potential for pivoting to internal networks.",
|
||||
"remediation": "1. Implement URL whitelist\n2. Block requests to internal IPs\n3. Disable unnecessary URL schemes\n4. Use network segmentation"
|
||||
},
|
||||
"ssrf_cloud": {
|
||||
"title": "SSRF to Cloud Metadata",
|
||||
"severity": "critical",
|
||||
"cwe_id": "CWE-918",
|
||||
"description": "SSRF vulnerability allowing access to cloud provider metadata services.",
|
||||
"impact": "Credential theft, full cloud account compromise, lateral movement in cloud infrastructure.",
|
||||
"remediation": "1. Block requests to metadata IPs\n2. Use IMDSv2 (AWS) or equivalent\n3. Implement strict URL validation\n4. Use firewall rules for metadata endpoints"
|
||||
},
|
||||
"csrf": {
|
||||
"title": "Cross-Site Request Forgery",
|
||||
"severity": "medium",
|
||||
"cwe_id": "CWE-352",
|
||||
"description": "Application allows state-changing requests without proper origin validation.",
|
||||
"impact": "Attacker can perform actions as authenticated users, including transfers, password changes, or data modification.",
|
||||
"remediation": "1. Implement anti-CSRF tokens\n2. Verify Origin/Referer headers\n3. Use SameSite cookie attribute\n4. Require re-authentication for sensitive actions"
|
||||
},
|
||||
|
||||
# Authentication
|
||||
"auth_bypass": {
|
||||
"title": "Authentication Bypass",
|
||||
"severity": "critical",
|
||||
"cwe_id": "CWE-287",
|
||||
"description": "Authentication mechanisms can be bypassed through various techniques.",
|
||||
"impact": "Complete unauthorized access to user accounts and protected resources.",
|
||||
"remediation": "1. Implement proper authentication checks on all routes\n2. Use proven authentication frameworks\n3. Implement account lockout\n4. Use MFA for sensitive accounts"
|
||||
},
|
||||
"jwt_manipulation": {
|
||||
"title": "JWT Token Manipulation",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-347",
|
||||
"description": "JWT implementation vulnerabilities allowing token forgery or manipulation.",
|
||||
"impact": "Authentication bypass, privilege escalation, and identity impersonation.",
|
||||
"remediation": "1. Always verify JWT signatures\n2. Use strong signing algorithms (RS256)\n3. Validate all claims including exp and iss\n4. Implement token refresh mechanisms"
|
||||
},
|
||||
"session_fixation": {
|
||||
"title": "Session Fixation",
|
||||
"severity": "medium",
|
||||
"cwe_id": "CWE-384",
|
||||
"description": "Application accepts session tokens from URL parameters or doesn't regenerate after login.",
|
||||
"impact": "Attacker can hijack user sessions by fixing known session IDs.",
|
||||
"remediation": "1. Regenerate session ID after login\n2. Only accept session from cookies\n3. Implement secure session management\n4. Use short session timeouts"
|
||||
},
|
||||
|
||||
# Authorization
|
||||
"idor": {
|
||||
"title": "Insecure Direct Object Reference",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-639",
|
||||
"description": "Application exposes internal object IDs without proper authorization checks.",
|
||||
"impact": "Unauthorized access to other users' data, potentially exposing sensitive information.",
|
||||
"remediation": "1. Implement proper authorization checks\n2. Use indirect references or UUIDs\n3. Validate user ownership of resources\n4. Implement access control lists"
|
||||
},
|
||||
"bola": {
|
||||
"title": "Broken Object Level Authorization",
|
||||
"severity": "high",
|
||||
"cwe_id": "CWE-639",
|
||||
"description": "API endpoints don't properly validate object-level permissions.",
|
||||
"impact": "Access to any object by manipulating IDs, leading to mass data exposure.",
|
||||
"remediation": "1. Implement object-level authorization\n2. Validate permissions on every request\n3. Use authorization middleware\n4. Log and monitor access patterns"
|
||||
},
|
||||
"privilege_escalation": {
|
||||
"title": "Privilege Escalation",
|
||||
"severity": "critical",
|
||||
"cwe_id": "CWE-269",
|
||||
"description": "User can elevate privileges to access higher-level functionality.",
|
||||
"impact": "User can gain admin access, access to all data, and full system control.",
|
||||
"remediation": "1. Implement role-based access control\n2. Validate roles on every request\n3. Use principle of least privilege\n4. Monitor for privilege escalation attempts"
|
||||
},
|
||||
|
||||
# Client-side
|
||||
"cors_misconfig": {
|
||||
"title": "CORS Misconfiguration",
|
||||
"severity": "medium",
|
||||
"cwe_id": "CWE-942",
|
||||
"description": "Overly permissive CORS policy allows cross-origin requests from untrusted domains.",
|
||||
"impact": "Cross-origin data theft and unauthorized API access from malicious websites.",
|
||||
"remediation": "1. Implement strict origin whitelist\n2. Avoid Access-Control-Allow-Origin: *\n3. Validate Origin header server-side\n4. Don't reflect Origin without validation"
|
||||
},
|
||||
"clickjacking": {
|
||||
"title": "Clickjacking",
|
||||
"severity": "medium",
|
||||
"cwe_id": "CWE-1021",
|
||||
"description": "Application can be framed by malicious pages, tricking users into clicking hidden elements.",
|
||||
"impact": "Users can be tricked into performing unintended actions like transfers or permission grants.",
|
||||
"remediation": "1. Set X-Frame-Options: DENY\n2. Implement frame-ancestors CSP directive\n3. Use JavaScript frame-busting as backup\n4. Require confirmation for sensitive actions"
|
||||
},
|
||||
"open_redirect": {
|
||||
"title": "Open Redirect",
|
||||
"severity": "low",
|
||||
"cwe_id": "CWE-601",
|
||||
"description": "Application redirects to user-specified URLs without validation.",
|
||||
"impact": "Phishing attacks using trusted domain, credential theft, and reputation damage.",
|
||||
"remediation": "1. Use whitelist for redirect destinations\n2. Validate redirect URLs server-side\n3. Don't use user input directly in redirects\n4. Warn users before redirecting externally"
|
||||
},
|
||||
|
||||
# Infrastructure
|
||||
"security_headers": {
|
||||
"title": "Missing Security Headers",
|
||||
"severity": "low",
|
||||
"cwe_id": "CWE-693",
|
||||
"description": "Application doesn't set important security headers like CSP, HSTS, X-Frame-Options.",
|
||||
"impact": "Increased risk of XSS, clickjacking, and MITM attacks.",
|
||||
"remediation": "1. Implement Content-Security-Policy\n2. Enable Strict-Transport-Security\n3. Set X-Frame-Options and X-Content-Type-Options\n4. Configure Referrer-Policy"
|
||||
},
|
||||
"ssl_issues": {
|
||||
"title": "SSL/TLS Configuration Issues",
|
||||
"severity": "medium",
|
||||
"cwe_id": "CWE-326",
|
||||
"description": "Weak SSL/TLS configuration including outdated protocols or weak ciphers.",
|
||||
"impact": "Traffic interception, credential theft, and man-in-the-middle attacks.",
|
||||
"remediation": "1. Disable SSLv3, TLS 1.0, TLS 1.1\n2. Use strong cipher suites only\n3. Enable HSTS with preload\n4. Implement certificate pinning for mobile apps"
|
||||
},
|
||||
"http_methods": {
|
||||
"title": "Dangerous HTTP Methods Enabled",
|
||||
"severity": "low",
|
||||
"cwe_id": "CWE-749",
|
||||
"description": "Server allows potentially dangerous HTTP methods like TRACE, PUT, DELETE without proper restrictions.",
|
||||
"impact": "Potential for XST attacks, unauthorized file uploads, or resource manipulation.",
|
||||
"remediation": "1. Disable unnecessary HTTP methods\n2. Configure web server to reject TRACE/TRACK\n3. Implement proper authorization for PUT/DELETE\n4. Use web application firewall"
|
||||
},
|
||||
|
||||
# Logic
|
||||
"race_condition": {
|
||||
"title": "Race Condition",
|
||||
"severity": "medium",
|
||||
"cwe_id": "CWE-362",
|
||||
"description": "Application has race conditions that can be exploited through concurrent requests.",
|
||||
"impact": "Double-spending, bypassing limits, or corrupting data through timing attacks.",
|
||||
"remediation": "1. Implement proper locking mechanisms\n2. Use atomic database operations\n3. Implement idempotency keys\n4. Add proper synchronization"
|
||||
},
|
||||
"business_logic": {
|
||||
"title": "Business Logic Vulnerability",
|
||||
"severity": "varies",
|
||||
"cwe_id": "CWE-840",
|
||||
"description": "Flaw in application's business logic allowing unintended behavior.",
|
||||
"impact": "Varies based on specific flaw - could range from minor to critical impact.",
|
||||
"remediation": "1. Review business logic flows\n2. Implement comprehensive validation\n3. Add server-side checks for all rules\n4. Test edge cases and negative scenarios"
|
||||
},
|
||||
|
||||
# ===== NEW TYPES (68 additional) =====
|
||||
|
||||
# Advanced Injection
|
||||
"ldap_injection": {"title": "LDAP Injection", "severity": "high", "cwe_id": "CWE-90", "description": "User input injected into LDAP queries allowing directory enumeration or auth bypass.", "impact": "Directory enumeration, authentication bypass, data extraction from LDAP stores.", "remediation": "1. Escape LDAP special characters\n2. Use parameterized LDAP queries\n3. Validate input against whitelist\n4. Apply least privilege to LDAP accounts"},
|
||||
"xpath_injection": {"title": "XPath Injection", "severity": "high", "cwe_id": "CWE-643", "description": "User input injected into XPath queries manipulating XML data retrieval.", "impact": "Extraction of XML data, authentication bypass via XPath condition manipulation.", "remediation": "1. Use parameterized XPath queries\n2. Validate and sanitize input\n3. Avoid string concatenation in XPath\n4. Limit XPath query privileges"},
|
||||
"graphql_injection": {"title": "GraphQL Injection", "severity": "high", "cwe_id": "CWE-89", "description": "Injection attacks targeting GraphQL endpoints through malicious queries or variables.", "impact": "Schema exposure, unauthorized data access, denial of service via complex queries.", "remediation": "1. Disable introspection in production\n2. Implement query depth/complexity limits\n3. Use persisted queries\n4. Apply field-level authorization"},
|
||||
"crlf_injection": {"title": "CRLF Injection / HTTP Response Splitting", "severity": "medium", "cwe_id": "CWE-93", "description": "Injection of CRLF characters to manipulate HTTP response headers or split responses.", "impact": "HTTP header injection, session fixation via Set-Cookie, XSS via response splitting.", "remediation": "1. Strip \\r\\n from user input in headers\n2. Use framework header-setting functions\n3. Validate header values\n4. Implement WAF rules for CRLF patterns"},
|
||||
"header_injection": {"title": "HTTP Header Injection", "severity": "medium", "cwe_id": "CWE-113", "description": "User input reflected in HTTP headers enabling header manipulation.", "impact": "Password reset poisoning, cache poisoning, access control bypass via header manipulation.", "remediation": "1. Validate Host header against whitelist\n2. Don't use Host header for URL generation\n3. Strip CRLF from header values\n4. Use absolute URLs for sensitive operations"},
|
||||
"email_injection": {"title": "Email Header Injection", "severity": "medium", "cwe_id": "CWE-93", "description": "Injection of email headers through form fields that feed into mail functions.", "impact": "Spam relay, phishing via injected CC/BCC recipients, email content manipulation.", "remediation": "1. Validate email addresses strictly\n2. Strip CRLF from email inputs\n3. Use email library APIs not raw headers\n4. Implement rate limiting on email features"},
|
||||
"expression_language_injection": {"title": "Expression Language Injection", "severity": "critical", "cwe_id": "CWE-917", "description": "Injection of EL/SpEL/OGNL expressions evaluated server-side in Java applications.", "impact": "Remote code execution, server compromise, data exfiltration via expression evaluation.", "remediation": "1. Disable EL evaluation on user input\n2. Use strict sandboxing\n3. Update frameworks (Struts2 OGNL patches)\n4. Validate input before template rendering"},
|
||||
"log_injection": {"title": "Log Injection / Log4Shell", "severity": "high", "cwe_id": "CWE-117", "description": "Injection into application logs enabling log forging or JNDI-based RCE (Log4Shell).", "impact": "Log tampering, JNDI-based RCE (Log4Shell), log analysis tool exploitation.", "remediation": "1. Strip newlines from log input\n2. Update Log4j to 2.17+ (CVE-2021-44228)\n3. Disable JNDI lookups\n4. Use structured logging"},
|
||||
"html_injection": {"title": "HTML Injection", "severity": "medium", "cwe_id": "CWE-79", "description": "Injection of HTML markup into web pages without script execution.", "impact": "Content spoofing, phishing form injection, defacement, link manipulation.", "remediation": "1. HTML-encode all user output\n2. Use Content-Security-Policy\n3. Implement output encoding libraries\n4. Sanitize HTML with whitelist approach"},
|
||||
"csv_injection": {"title": "CSV/Formula Injection", "severity": "medium", "cwe_id": "CWE-1236", "description": "Injection of spreadsheet formulas into data exported as CSV/Excel.", "impact": "Code execution when CSV opened in Excel, DDE attacks, data exfiltration via formulas.", "remediation": "1. Prefix cells starting with =,+,-,@ with single quote\n2. Sanitize formula characters\n3. Use safe CSV export libraries\n4. Warn users about untrusted CSV files"},
|
||||
"orm_injection": {"title": "ORM Injection", "severity": "high", "cwe_id": "CWE-89", "description": "Injection through ORM query builders via operator injection or raw query manipulation.", "impact": "Data extraction, authentication bypass through ORM filter manipulation.", "remediation": "1. Use ORM built-in parameter binding\n2. Avoid raw queries with user input\n3. Validate filter operators\n4. Use field-level whitelists"},
|
||||
|
||||
# XSS Advanced
|
||||
"blind_xss": {"title": "Blind Cross-Site Scripting", "severity": "high", "cwe_id": "CWE-79", "description": "XSS payload stored and executed in backend/admin context not visible to the attacker.", "impact": "Admin session hijacking, backend system compromise, persistent access to admin panels.", "remediation": "1. Sanitize all input regardless of display context\n2. Implement CSP on admin panels\n3. Use HttpOnly cookies\n4. Review admin panel input rendering"},
|
||||
"mutation_xss": {"title": "Mutation XSS (mXSS)", "severity": "high", "cwe_id": "CWE-79", "description": "XSS via browser HTML mutation where sanitized HTML changes to executable form after DOM processing.", "impact": "Bypasses HTML sanitizers, executes JavaScript through browser parsing quirks.", "remediation": "1. Update DOMPurify/sanitizers\n2. Use textContent not innerHTML\n3. Avoid innerHTML re-serialization\n4. Test with multiple browsers"},
|
||||
|
||||
# File Access Advanced
|
||||
"arbitrary_file_read": {"title": "Arbitrary File Read", "severity": "high", "cwe_id": "CWE-22", "description": "Reading arbitrary files via API or download endpoints outside intended scope.", "impact": "Access to credentials, configuration, source code, private keys.", "remediation": "1. Validate file paths against whitelist\n2. Use chroot/jail\n3. Implement proper access controls\n4. Avoid user input in file paths"},
|
||||
"arbitrary_file_delete": {"title": "Arbitrary File Delete", "severity": "high", "cwe_id": "CWE-22", "description": "Deleting arbitrary files through path traversal in delete operations.", "impact": "Denial of service, security bypass by deleting .htaccess/config, data destruction.", "remediation": "1. Validate file paths strictly\n2. Use indirect references\n3. Implement soft-delete\n4. Restrict delete operations to specific directories"},
|
||||
"zip_slip": {"title": "Zip Slip (Archive Path Traversal)", "severity": "high", "cwe_id": "CWE-22", "description": "Path traversal via crafted archive filenames writing files outside extraction directory.", "impact": "Arbitrary file write, web shell deployment, configuration overwrite.", "remediation": "1. Validate archive entry names\n2. Resolve and check extraction paths\n3. Use secure archive extraction libraries\n4. Extract to isolated directories"},
|
||||
|
||||
# Auth Advanced
|
||||
"weak_password": {"title": "Weak Password Policy", "severity": "medium", "cwe_id": "CWE-521", "description": "Application accepts weak passwords that can be easily guessed or brute-forced.", "impact": "Account compromise through password guessing, credential stuffing success.", "remediation": "1. Enforce minimum 8+ character passwords\n2. Check against breached password databases\n3. Implement password strength meter\n4. Follow NIST SP 800-63B guidelines"},
|
||||
"default_credentials": {"title": "Default Credentials", "severity": "critical", "cwe_id": "CWE-798", "description": "Application or service uses default factory credentials that haven't been changed.", "impact": "Complete unauthorized access to admin or management interfaces.", "remediation": "1. Force password change on first login\n2. Remove default accounts\n3. Implement strong default password generation\n4. Regular credential audits"},
|
||||
"brute_force": {"title": "Brute Force Vulnerability", "severity": "medium", "cwe_id": "CWE-307", "description": "Login endpoint lacks rate limiting or account lockout allowing unlimited password attempts.", "impact": "Account compromise through automated password guessing.", "remediation": "1. Implement account lockout after N failures\n2. Add rate limiting per IP and per account\n3. Implement CAPTCHA after failures\n4. Use progressive delays"},
|
||||
"two_factor_bypass": {"title": "Two-Factor Authentication Bypass", "severity": "high", "cwe_id": "CWE-287", "description": "Second authentication factor can be bypassed through implementation flaws.", "impact": "Account takeover even when 2FA is enabled, defeating the purpose of MFA.", "remediation": "1. Enforce 2FA check on all authenticated routes\n2. Use server-side session state for 2FA completion\n3. Rate limit code attempts\n4. Make codes single-use with short expiry"},
|
||||
"oauth_misconfiguration": {"title": "OAuth Misconfiguration", "severity": "high", "cwe_id": "CWE-601", "description": "OAuth implementation flaws allowing redirect URI manipulation, state bypass, or token theft.", "impact": "Account takeover via stolen OAuth tokens, cross-site request forgery.", "remediation": "1. Strictly validate redirect_uri\n2. Require and validate state parameter\n3. Use PKCE for public clients\n4. Validate all OAuth scopes"},
|
||||
|
||||
# Authorization Advanced
|
||||
"bfla": {"title": "Broken Function Level Authorization", "severity": "high", "cwe_id": "CWE-285", "description": "Admin API functions accessible to regular users without proper role checks.", "impact": "Privilege escalation to admin functionality, system configuration changes.", "remediation": "1. Implement role-based access control on all endpoints\n2. Deny by default\n3. Centralize authorization logic\n4. Audit all admin endpoints"},
|
||||
"mass_assignment": {"title": "Mass Assignment", "severity": "high", "cwe_id": "CWE-915", "description": "Application binds user-supplied data to internal model fields without filtering.", "impact": "Privilege escalation, data manipulation, bypassing business rules.", "remediation": "1. Use explicit field whitelists\n2. Implement DTOs for input\n3. Validate all bound fields\n4. Use strong parameter filtering"},
|
||||
"forced_browsing": {"title": "Forced Browsing / Broken Access Control", "severity": "medium", "cwe_id": "CWE-425", "description": "Direct URL access to restricted resources that should require authorization.", "impact": "Access to admin panels, sensitive files, debug interfaces, and internal tools.", "remediation": "1. Implement authentication on all protected routes\n2. Return 404 instead of 403 for sensitive paths\n3. Remove unnecessary files\n4. Use web server access controls"},
|
||||
|
||||
# Client-Side Advanced
|
||||
"dom_clobbering": {"title": "DOM Clobbering", "severity": "medium", "cwe_id": "CWE-79", "description": "HTML injection that overrides JavaScript DOM properties through named elements.", "impact": "JavaScript logic bypass, potential XSS through clobbered variables.", "remediation": "1. Use strict variable declarations (const/let)\n2. Avoid global variable references\n3. Use safe DOM APIs\n4. Sanitize HTML input"},
|
||||
"postmessage_vulnerability": {"title": "postMessage Vulnerability", "severity": "medium", "cwe_id": "CWE-346", "description": "postMessage handlers that don't validate message origin allowing cross-origin data injection.", "impact": "Cross-origin data injection, XSS via injected data, sensitive data exfiltration.", "remediation": "1. Always validate event.origin\n2. Validate message data structure\n3. Use specific target origins\n4. Minimize data sent via postMessage"},
|
||||
"websocket_hijacking": {"title": "Cross-Site WebSocket Hijacking", "severity": "high", "cwe_id": "CWE-1385", "description": "WebSocket endpoints accepting connections from arbitrary origins without validation.", "impact": "Real-time data theft, message injection, session hijacking via WebSocket.", "remediation": "1. Validate Origin header on WebSocket upgrade\n2. Require authentication per-message\n3. Implement CSRF protection for handshake\n4. Use WSS (encrypted)"},
|
||||
"prototype_pollution": {"title": "Prototype Pollution", "severity": "high", "cwe_id": "CWE-1321", "description": "Injection of properties into JavaScript Object.prototype through merge/extend operations.", "impact": "Authentication bypass, RCE via gadget chains, denial of service.", "remediation": "1. Freeze Object.prototype\n2. Sanitize __proto__ and constructor keys\n3. Use Map instead of plain objects\n4. Update vulnerable libraries"},
|
||||
"css_injection": {"title": "CSS Injection", "severity": "medium", "cwe_id": "CWE-79", "description": "Injection of CSS code through user input reflected in style contexts.", "impact": "Data exfiltration via CSS selectors, UI manipulation, phishing.", "remediation": "1. Sanitize CSS properties\n2. Use CSP style-src\n3. Avoid user input in style attributes\n4. Whitelist safe CSS properties"},
|
||||
"tabnabbing": {"title": "Reverse Tabnabbing", "severity": "low", "cwe_id": "CWE-1022", "description": "Links with target=_blank without rel=noopener allowing opener tab navigation.", "impact": "Phishing via original tab replacement with fake login page.", "remediation": "1. Add rel='noopener noreferrer' to target=_blank links\n2. Use frameworks that add it automatically\n3. Audit user-generated links"},
|
||||
|
||||
# Infrastructure Advanced
|
||||
"directory_listing": {"title": "Directory Listing Enabled", "severity": "low", "cwe_id": "CWE-548", "description": "Web server auto-indexing enabled exposing directory file structure.", "impact": "Exposure of file structure, sensitive files, backup files, and configuration.", "remediation": "1. Disable directory listing (Options -Indexes)\n2. Add index files to all directories\n3. Review web server configuration\n4. Use custom error pages"},
|
||||
"debug_mode": {"title": "Debug Mode Enabled", "severity": "high", "cwe_id": "CWE-489", "description": "Application running in debug/development mode in production.", "impact": "Source code exposure, interactive console access, credential disclosure.", "remediation": "1. Disable debug mode in production\n2. Use environment-specific configuration\n3. Implement custom error pages\n4. Remove debug endpoints"},
|
||||
"exposed_admin_panel": {"title": "Exposed Administration Panel", "severity": "medium", "cwe_id": "CWE-200", "description": "Admin panel accessible from public internet without IP restrictions.", "impact": "Brute force target, credential theft, administration access if default creds.", "remediation": "1. Restrict admin access by IP/VPN\n2. Use strong authentication + 2FA\n3. Change default admin paths\n4. Implement rate limiting"},
|
||||
"exposed_api_docs": {"title": "Exposed API Documentation", "severity": "low", "cwe_id": "CWE-200", "description": "API documentation (Swagger/OpenAPI/GraphQL playground) publicly accessible.", "impact": "Complete API endpoint mapping, parameter discovery, potential unauthorized access.", "remediation": "1. Disable API docs in production\n2. Require authentication for docs\n3. Disable GraphQL introspection\n4. Use API gateway access controls"},
|
||||
"insecure_cookie_flags": {"title": "Insecure Cookie Configuration", "severity": "medium", "cwe_id": "CWE-614", "description": "Session cookies missing security flags (Secure, HttpOnly, SameSite).", "impact": "Cookie theft via XSS (no HttpOnly), MITM (no Secure), CSRF (no SameSite).", "remediation": "1. Set HttpOnly on session cookies\n2. Set Secure flag on HTTPS sites\n3. Set SameSite=Lax or Strict\n4. Review all cookie configurations"},
|
||||
"http_smuggling": {"title": "HTTP Request Smuggling", "severity": "high", "cwe_id": "CWE-444", "description": "Discrepancy between front-end and back-end HTTP parsing enabling request smuggling.", "impact": "Cache poisoning, request hijacking, authentication bypass, response queue poisoning.", "remediation": "1. Use HTTP/2 end-to-end\n2. Normalize Content-Length/Transfer-Encoding\n3. Reject ambiguous requests\n4. Update proxy/server software"},
|
||||
"cache_poisoning": {"title": "Web Cache Poisoning", "severity": "high", "cwe_id": "CWE-444", "description": "Manipulation of cached responses via unkeyed inputs to serve malicious content.", "impact": "Mass XSS via cached responses, redirect poisoning, denial of service.", "remediation": "1. Include all inputs in cache key\n2. Validate unkeyed headers\n3. Use Vary header correctly\n4. Implement cache key normalization"},
|
||||
|
||||
# Logic & Data
|
||||
"rate_limit_bypass": {"title": "Rate Limit Bypass", "severity": "medium", "cwe_id": "CWE-770", "description": "Rate limiting can be bypassed through header manipulation or request variation.", "impact": "Enables brute force attacks, API abuse, and denial of service.", "remediation": "1. Rate limit by authenticated user, not just IP\n2. Don't trust X-Forwarded-For for rate limiting\n3. Implement at multiple layers\n4. Use sliding window algorithms"},
|
||||
"parameter_pollution": {"title": "HTTP Parameter Pollution", "severity": "medium", "cwe_id": "CWE-235", "description": "Duplicate parameters exploit parsing differences between front-end and back-end.", "impact": "WAF bypass, logic bypass, access control circumvention.", "remediation": "1. Normalize parameters server-side\n2. Reject duplicate parameters\n3. Use consistent parsing\n4. Test with duplicate params"},
|
||||
"type_juggling": {"title": "Type Juggling / Type Coercion", "severity": "high", "cwe_id": "CWE-843", "description": "Loose type comparison exploited to bypass authentication or security checks.", "impact": "Authentication bypass, security check circumvention via type confusion.", "remediation": "1. Use strict comparison (=== in PHP/JS)\n2. Validate input types\n3. Use strong typing\n4. Hash comparison with timing-safe functions"},
|
||||
"insecure_deserialization": {"title": "Insecure Deserialization", "severity": "critical", "cwe_id": "CWE-502", "description": "Untrusted data deserialized without validation enabling code execution.", "impact": "Remote code execution, denial of service, authentication bypass.", "remediation": "1. Don't deserialize untrusted data\n2. Use JSON instead of native serialization\n3. Implement integrity checks\n4. Restrict deserialization types"},
|
||||
"subdomain_takeover": {"title": "Subdomain Takeover", "severity": "high", "cwe_id": "CWE-284", "description": "Dangling DNS records pointing to unclaimed cloud resources.", "impact": "Domain impersonation, phishing, cookie theft, authentication bypass.", "remediation": "1. Audit DNS records regularly\n2. Remove dangling CNAME records\n3. Monitor cloud resource lifecycle\n4. Use DNS monitoring tools"},
|
||||
"host_header_injection": {"title": "Host Header Injection", "severity": "medium", "cwe_id": "CWE-644", "description": "Host header value used in URL generation enabling poisoning attacks.", "impact": "Password reset poisoning, cache poisoning, SSRF via Host header.", "remediation": "1. Validate Host against allowed values\n2. Use absolute URLs from configuration\n3. Don't use Host header for URL generation\n4. Implement ALLOWED_HOSTS"},
|
||||
"timing_attack": {"title": "Timing Attack", "severity": "medium", "cwe_id": "CWE-208", "description": "Response time variations leak information about valid usernames or secret values.", "impact": "Username enumeration, token/password character extraction.", "remediation": "1. Use constant-time comparison for secrets\n2. Normalize response times\n3. Add random delays\n4. Use same code path for valid/invalid input"},
|
||||
"improper_error_handling": {"title": "Improper Error Handling", "severity": "low", "cwe_id": "CWE-209", "description": "Verbose error messages disclosing internal information in production.", "impact": "Source path disclosure, database details, technology stack exposure aiding further attacks.", "remediation": "1. Use custom error pages in production\n2. Log errors server-side only\n3. Return generic error messages\n4. Disable debug/stack trace output"},
|
||||
"sensitive_data_exposure": {"title": "Sensitive Data Exposure", "severity": "high", "cwe_id": "CWE-200", "description": "Sensitive data (PII, credentials, tokens) exposed in responses, URLs, or storage.", "impact": "Identity theft, account compromise, regulatory violations (GDPR, HIPAA).", "remediation": "1. Minimize data in API responses\n2. Encrypt sensitive data at rest/transit\n3. Remove sensitive data from URLs\n4. Implement data classification"},
|
||||
"information_disclosure": {"title": "Information Disclosure", "severity": "low", "cwe_id": "CWE-200", "description": "Unintended exposure of internal details: versions, paths, technology stack.", "impact": "Aids further attacks with technology-specific exploits and internal knowledge.", "remediation": "1. Remove version headers\n2. Disable directory listing\n3. Remove HTML comments\n4. Secure .git and config files"},
|
||||
"api_key_exposure": {"title": "API Key Exposure", "severity": "high", "cwe_id": "CWE-798", "description": "API keys or secrets hardcoded in client-side code or public files.", "impact": "Unauthorized API access, financial impact, data breach via exposed keys.", "remediation": "1. Use environment variables for secrets\n2. Implement key rotation\n3. Use backend proxy for API calls\n4. Monitor key usage for anomalies"},
|
||||
"source_code_disclosure": {"title": "Source Code Disclosure", "severity": "high", "cwe_id": "CWE-540", "description": "Application source code accessible through misconfigured servers, backups, or VCS exposure.", "impact": "White-box attack surface, credential discovery, vulnerability identification.", "remediation": "1. Block .git, .svn access\n2. Remove source maps in production\n3. Delete backup files\n4. Configure web server to block sensitive extensions"},
|
||||
"backup_file_exposure": {"title": "Backup File Exposure", "severity": "high", "cwe_id": "CWE-530", "description": "Backup files, database dumps, or archives accessible from web server.", "impact": "Full source code access, database contents including credentials.", "remediation": "1. Store backups outside web root\n2. Remove old backup files\n3. Block backup extensions in web server\n4. Encrypt backup files"},
|
||||
"version_disclosure": {"title": "Software Version Disclosure", "severity": "low", "cwe_id": "CWE-200", "description": "Specific software versions exposed enabling targeted CVE exploitation.", "impact": "Targeted exploitation of known vulnerabilities for the specific version.", "remediation": "1. Remove version from headers\n2. Update software regularly\n3. Remove version-disclosing files\n4. Customize error pages"},
|
||||
|
||||
# Crypto & Supply
|
||||
"weak_encryption": {"title": "Weak Encryption Algorithm", "severity": "medium", "cwe_id": "CWE-327", "description": "Use of weak/deprecated encryption algorithms (DES, RC4, ECB mode).", "impact": "Data decryption, MITM attacks, breaking confidentiality protections.", "remediation": "1. Use AES-256-GCM or ChaCha20\n2. Disable weak cipher suites\n3. Use TLS 1.2+ only\n4. Regular cryptographic review"},
|
||||
"weak_hashing": {"title": "Weak Hashing Algorithm", "severity": "medium", "cwe_id": "CWE-328", "description": "Use of weak hash algorithms (MD5, SHA1) for security-critical purposes.", "impact": "Password cracking, hash collision attacks, integrity bypass.", "remediation": "1. Use bcrypt/scrypt/argon2 for passwords\n2. Use SHA-256+ for integrity\n3. Always use salts\n4. Implement key stretching"},
|
||||
"weak_random": {"title": "Weak Random Number Generation", "severity": "medium", "cwe_id": "CWE-330", "description": "Predictable random numbers used for security tokens or session IDs.", "impact": "Token prediction, session hijacking, CSRF token bypass.", "remediation": "1. Use cryptographic PRNG (secrets module, SecureRandom)\n2. Avoid Math.random() for security\n3. Use sufficient entropy\n4. Regular token rotation"},
|
||||
"cleartext_transmission": {"title": "Cleartext Transmission of Sensitive Data", "severity": "medium", "cwe_id": "CWE-319", "description": "Sensitive data transmitted over unencrypted HTTP connections.", "impact": "Credential theft via MITM, session hijacking, data exposure.", "remediation": "1. Enforce HTTPS everywhere\n2. Implement HSTS with preload\n3. Redirect HTTP to HTTPS\n4. Set Secure flag on cookies"},
|
||||
"vulnerable_dependency": {"title": "Vulnerable Third-Party Dependency", "severity": "varies", "cwe_id": "CWE-1104", "description": "Third-party library with known CVEs in use.", "impact": "Depends on specific CVE - from XSS to RCE.", "remediation": "1. Regular dependency updates\n2. Use automated vulnerability scanning\n3. Monitor CVE advisories\n4. Implement SCA in CI/CD"},
|
||||
"outdated_component": {"title": "Outdated Software Component", "severity": "medium", "cwe_id": "CWE-1104", "description": "Significantly outdated CMS, framework, or server with multiple known CVEs.", "impact": "Multiple exploitable vulnerabilities, targeted attacks.", "remediation": "1. Update to latest stable version\n2. Enable automatic security updates\n3. Monitor end-of-life announcements\n4. Implement patch management"},
|
||||
"insecure_cdn": {"title": "Insecure CDN Resource Loading", "severity": "low", "cwe_id": "CWE-829", "description": "External scripts loaded without Subresource Integrity (SRI) hashes.", "impact": "Supply chain attack via CDN compromise, mass XSS.", "remediation": "1. Add integrity= attribute to script/link tags\n2. Use crossorigin attribute\n3. Self-host critical resources\n4. Implement CSP with hash sources"},
|
||||
"container_escape": {"title": "Container Escape / Misconfiguration", "severity": "critical", "cwe_id": "CWE-250", "description": "Container running with elevated privileges or exposed host resources.", "impact": "Host system compromise, lateral movement, data access across containers.", "remediation": "1. Don't use --privileged\n2. Drop unnecessary capabilities\n3. Don't mount Docker socket\n4. Use seccomp/AppArmor profiles"},
|
||||
|
||||
# Cloud & API
|
||||
"s3_bucket_misconfiguration": {"title": "S3/Cloud Storage Misconfiguration", "severity": "high", "cwe_id": "CWE-284", "description": "Cloud storage bucket with public read/write access.", "impact": "Data exposure, data tampering, hosting malicious content.", "remediation": "1. Enable S3 Block Public Access\n2. Review bucket policies\n3. Use IAM policies for access\n4. Enable access logging"},
|
||||
"cloud_metadata_exposure": {"title": "Cloud Metadata Exposure", "severity": "critical", "cwe_id": "CWE-918", "description": "Cloud instance metadata service accessible exposing credentials.", "impact": "IAM credential theft, cloud account compromise, lateral movement.", "remediation": "1. Use IMDSv2 (token-required)\n2. Block metadata endpoint in firewall\n3. Implement SSRF protection\n4. Use minimal IAM roles"},
|
||||
"serverless_misconfiguration": {"title": "Serverless Misconfiguration", "severity": "medium", "cwe_id": "CWE-284", "description": "Serverless function with excessive permissions or missing auth.", "impact": "Unauthorized function execution, environment variable exposure, privilege escalation.", "remediation": "1. Apply least privilege IAM roles\n2. Require authentication\n3. Don't expose secrets in env vars\n4. Implement function authorization"},
|
||||
"graphql_introspection": {"title": "GraphQL Introspection Enabled", "severity": "low", "cwe_id": "CWE-200", "description": "GraphQL introspection enabled in production exposing full API schema.", "impact": "Complete API mapping, discovery of sensitive types and mutations.", "remediation": "1. Disable introspection in production\n2. Use persisted queries\n3. Implement field-level authorization\n4. Use query allowlisting"},
|
||||
"graphql_dos": {"title": "GraphQL Denial of Service", "severity": "medium", "cwe_id": "CWE-400", "description": "GraphQL endpoint vulnerable to resource-exhaustion via complex/nested queries.", "impact": "Service unavailability, resource exhaustion, increased infrastructure costs.", "remediation": "1. Implement query depth limits\n2. Add query complexity analysis\n3. Set timeout on queries\n4. Use persisted/allowlisted queries"},
|
||||
"rest_api_versioning": {"title": "Insecure API Version Exposure", "severity": "low", "cwe_id": "CWE-284", "description": "Older API versions with weaker security controls still accessible.", "impact": "Bypass newer security controls via old API versions.", "remediation": "1. Deprecate and remove old API versions\n2. Apply same security to all versions\n3. Monitor old version usage\n4. Set deprecation timelines"},
|
||||
"soap_injection": {"title": "SOAP/XML Web Service Injection", "severity": "high", "cwe_id": "CWE-91", "description": "Injection in SOAP/XML web service parameters manipulating queries.", "impact": "Data extraction, XXE via SOAP, SOAP action spoofing for unauthorized operations.", "remediation": "1. Validate SOAP input\n2. Disable XML external entities\n3. Validate SOAPAction header\n4. Use WS-Security"},
|
||||
"api_rate_limiting": {"title": "Missing API Rate Limiting", "severity": "medium", "cwe_id": "CWE-770", "description": "API endpoints lacking rate limiting allowing unlimited requests.", "impact": "Brute force, scraping, DoS, API abuse at scale.", "remediation": "1. Implement rate limiting per user/IP\n2. Return 429 with Retry-After\n3. Use API gateway throttling\n4. Implement sliding window algorithm"},
|
||||
"excessive_data_exposure": {"title": "Excessive Data Exposure", "severity": "medium", "cwe_id": "CWE-213", "description": "APIs returning more data than the client needs, including sensitive fields.", "impact": "Exposure of sensitive fields (password hashes, tokens, PII) to clients.", "remediation": "1. Use response DTOs/serializers\n2. Implement field-level filtering\n3. Apply least-data principle\n4. Separate admin and user endpoints"}
|
||||
}
|
||||
|
||||
# Tester class mappings (100 types)
|
||||
TESTER_CLASSES = {
|
||||
# Injection (10 original + 11 advanced)
|
||||
"xss_reflected": XSSReflectedTester,
|
||||
"xss_stored": XSSStoredTester,
|
||||
"xss_dom": XSSDomTester,
|
||||
"sqli_error": SQLiErrorTester,
|
||||
"sqli_union": SQLiUnionTester,
|
||||
"sqli_blind": SQLiBlindTester,
|
||||
"sqli_time": SQLiTimeTester,
|
||||
"command_injection": CommandInjectionTester,
|
||||
"ssti": SSTITester,
|
||||
"nosql_injection": NoSQLInjectionTester,
|
||||
"ldap_injection": LdapInjectionTester,
|
||||
"xpath_injection": XpathInjectionTester,
|
||||
"graphql_injection": GraphqlInjectionTester,
|
||||
"crlf_injection": CrlfInjectionTester,
|
||||
"header_injection": HeaderInjectionTester,
|
||||
"email_injection": EmailInjectionTester,
|
||||
"expression_language_injection": ELInjectionTester,
|
||||
"log_injection": LogInjectionTester,
|
||||
"html_injection": HtmlInjectionTester,
|
||||
"csv_injection": CsvInjectionTester,
|
||||
"orm_injection": OrmInjectionTester,
|
||||
|
||||
# XSS Advanced
|
||||
"blind_xss": XSSStoredTester, # Similar detection pattern
|
||||
"mutation_xss": XSSReflectedTester, # Similar detection pattern
|
||||
|
||||
# File Access (5 original + 3 new)
|
||||
"lfi": LFITester,
|
||||
"rfi": RFITester,
|
||||
"path_traversal": PathTraversalTester,
|
||||
"xxe": XXETester,
|
||||
"file_upload": FileUploadTester,
|
||||
"arbitrary_file_read": ArbitraryFileReadTester,
|
||||
"arbitrary_file_delete": ArbitraryFileDeleteTester,
|
||||
"zip_slip": ZipSlipTester,
|
||||
|
||||
# Request Forgery (3 original + 2 new)
|
||||
"ssrf": SSRFTester,
|
||||
"ssrf_cloud": SSRFTester,
|
||||
"csrf": CSRFTester,
|
||||
"cors_misconfig": CORSTester,
|
||||
"graphql_introspection": GraphqlIntrospectionTester,
|
||||
"graphql_dos": GraphqlDosTester,
|
||||
|
||||
# Auth (3 original + 5 new)
|
||||
"auth_bypass": AuthBypassTester,
|
||||
"jwt_manipulation": JWTManipulationTester,
|
||||
"session_fixation": SessionFixationTester,
|
||||
"weak_password": WeakPasswordTester,
|
||||
"default_credentials": DefaultCredentialsTester,
|
||||
"brute_force": AuthBypassTester, # Similar pattern
|
||||
"two_factor_bypass": TwoFactorBypassTester,
|
||||
"oauth_misconfiguration": OauthMisconfigTester,
|
||||
|
||||
# Authorization (3 original + 3 new)
|
||||
"idor": IDORTester,
|
||||
"bola": BOLATester,
|
||||
"privilege_escalation": PrivilegeEscalationTester,
|
||||
"bfla": BflaTester,
|
||||
"mass_assignment": MassAssignmentTester,
|
||||
"forced_browsing": ForcedBrowsingTester,
|
||||
|
||||
# Client-Side (3 original + 6 new)
|
||||
"clickjacking": ClickjackingTester,
|
||||
"open_redirect": OpenRedirectTester,
|
||||
"dom_clobbering": DomClobberingTester,
|
||||
"postmessage_vulnerability": PostMessageVulnTester,
|
||||
"websocket_hijacking": WebsocketHijackTester,
|
||||
"prototype_pollution": PrototypePollutionTester,
|
||||
"css_injection": CssInjectionTester,
|
||||
"tabnabbing": TabnabbingTester,
|
||||
|
||||
# Infrastructure (3 original + 7 new)
|
||||
"security_headers": SecurityHeadersTester,
|
||||
"ssl_issues": SSLTester,
|
||||
"http_methods": HTTPMethodsTester,
|
||||
"directory_listing": DirectoryListingTester,
|
||||
"debug_mode": DebugModeTester,
|
||||
"exposed_admin_panel": ExposedAdminPanelTester,
|
||||
"exposed_api_docs": ExposedApiDocsTester,
|
||||
"insecure_cookie_flags": InsecureCookieFlagsTester,
|
||||
"http_smuggling": HttpSmugglingTester,
|
||||
"cache_poisoning": CachePoisoningTester,
|
||||
|
||||
# Logic (9 types)
|
||||
"race_condition": RaceConditionTester,
|
||||
"business_logic": BusinessLogicTester,
|
||||
"rate_limit_bypass": RateLimitBypassTester,
|
||||
"parameter_pollution": ParameterPollutionTester,
|
||||
"type_juggling": TypeJugglingTester,
|
||||
"timing_attack": TimingAttackTester,
|
||||
"host_header_injection": HostHeaderInjectionTester,
|
||||
"insecure_deserialization": BaseTester, # AI-driven
|
||||
"subdomain_takeover": SubdomainTakeoverTester,
|
||||
"improper_error_handling": BaseTester, # AI-driven
|
||||
|
||||
# Data Exposure (6 types)
|
||||
"sensitive_data_exposure": SensitiveDataExposureTester,
|
||||
"information_disclosure": InformationDisclosureTester,
|
||||
"api_key_exposure": ApiKeyExposureTester,
|
||||
"source_code_disclosure": SourceCodeDisclosureTester,
|
||||
"backup_file_exposure": BackupFileExposureTester,
|
||||
"version_disclosure": VersionDisclosureTester,
|
||||
|
||||
# Crypto & Supply (8 types - mostly inspection/AI-driven)
|
||||
"weak_encryption": BaseTester,
|
||||
"weak_hashing": BaseTester,
|
||||
"weak_random": BaseTester,
|
||||
"cleartext_transmission": BaseTester,
|
||||
"vulnerable_dependency": VulnerableDependencyTester,
|
||||
"outdated_component": VulnerableDependencyTester,
|
||||
"insecure_cdn": BaseTester,
|
||||
"container_escape": ContainerEscapeTester,
|
||||
|
||||
# Cloud & API (7 types)
|
||||
"s3_bucket_misconfiguration": S3BucketMisconfigTester,
|
||||
"cloud_metadata_exposure": CloudMetadataExposureTester,
|
||||
"serverless_misconfiguration": ServerlessMisconfigTester,
|
||||
"rest_api_versioning": BaseTester, # AI-driven
|
||||
"soap_injection": BaseTester, # AI-driven
|
||||
"api_rate_limiting": RateLimitBypassTester,
|
||||
"excessive_data_exposure": SensitiveDataExposureTester,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._tester_cache = {}
|
||||
|
||||
def get_tester(self, vuln_type: str) -> BaseTester:
|
||||
"""Get tester instance for a vulnerability type"""
|
||||
if vuln_type in self._tester_cache:
|
||||
return self._tester_cache[vuln_type]
|
||||
|
||||
tester_class = self.TESTER_CLASSES.get(vuln_type, BaseTester)
|
||||
tester = tester_class()
|
||||
self._tester_cache[vuln_type] = tester
|
||||
return tester
|
||||
|
||||
def get_severity(self, vuln_type: str) -> str:
|
||||
"""Get severity for a vulnerability type"""
|
||||
info = self.VULNERABILITY_INFO.get(vuln_type, {})
|
||||
return info.get("severity", "medium")
|
||||
|
||||
def get_cwe_id(self, vuln_type: str) -> str:
|
||||
"""Get CWE ID for a vulnerability type"""
|
||||
info = self.VULNERABILITY_INFO.get(vuln_type, {})
|
||||
return info.get("cwe_id", "")
|
||||
|
||||
def get_title(self, vuln_type: str) -> str:
|
||||
"""Get title for a vulnerability type"""
|
||||
info = self.VULNERABILITY_INFO.get(vuln_type, {})
|
||||
return info.get("title", vuln_type.replace("_", " ").title())
|
||||
|
||||
def get_description(self, vuln_type: str) -> str:
|
||||
"""Get description for a vulnerability type"""
|
||||
info = self.VULNERABILITY_INFO.get(vuln_type, {})
|
||||
return info.get("description", "")
|
||||
|
||||
def get_impact(self, vuln_type: str) -> str:
|
||||
"""Get impact for a vulnerability type"""
|
||||
info = self.VULNERABILITY_INFO.get(vuln_type, {})
|
||||
return info.get("impact", "")
|
||||
|
||||
def get_remediation(self, vuln_type: str) -> str:
|
||||
"""Get remediation advice for a vulnerability type"""
|
||||
info = self.VULNERABILITY_INFO.get(vuln_type, {})
|
||||
return info.get("remediation", "")
|
||||
+1328
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
__all__ = ["BaseTester"]
|
||||
@@ -0,0 +1,532 @@
|
||||
"""
|
||||
NeuroSploit v3 - Advanced Injection Vulnerability Testers
|
||||
|
||||
Testers for LDAP, XPath, GraphQL, CRLF, Header, Email, EL, Log, HTML, CSV, and ORM injection.
|
||||
"""
|
||||
import re
|
||||
from typing import Tuple, Dict, Optional
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
|
||||
class LdapInjectionTester(BaseTester):
|
||||
"""Tester for LDAP Injection vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "ldap_injection"
|
||||
self.error_patterns = [
|
||||
r"javax\.naming\.NamingException",
|
||||
r"LDAPException",
|
||||
r"ldap_search\(\)",
|
||||
r"ldap_bind\(\)",
|
||||
r"Invalid DN syntax",
|
||||
r"Bad search filter",
|
||||
r"DSA is unavailable",
|
||||
r"LDAP error code \d+",
|
||||
r"cn=.*,\s*ou=.*,\s*dc=",
|
||||
r"objectClass=",
|
||||
r"No such object",
|
||||
r"invalid attribute description",
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for LDAP injection indicators"""
|
||||
# Check for LDAP error messages
|
||||
for pattern in self.error_patterns:
|
||||
match = re.search(pattern, response_body, re.IGNORECASE)
|
||||
if match:
|
||||
return True, 0.8, f"LDAP error detected: {match.group(0)[:100]}"
|
||||
|
||||
# Wildcard injection - check if directory listing returned
|
||||
if "*" in payload:
|
||||
# Multiple DN entries suggest directory enumeration
|
||||
dn_count = len(re.findall(r"dn:\s+\S+", response_body, re.IGNORECASE))
|
||||
if dn_count > 1:
|
||||
return True, 0.85, f"LDAP wildcard returned {dn_count} directory entries"
|
||||
|
||||
# Filter manipulation - check for unexpected data volume
|
||||
if ")(|" in payload or "*)(objectClass" in payload:
|
||||
if response_status == 200 and len(response_body) > 5000:
|
||||
return True, 0.6, "LDAP filter manipulation may have returned extra data"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class XpathInjectionTester(BaseTester):
|
||||
"""Tester for XPath Injection vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "xpath_injection"
|
||||
self.error_patterns = [
|
||||
r"XPathException",
|
||||
r"Invalid XPath",
|
||||
r"xpath syntax error",
|
||||
r"javax\.xml\.xpath",
|
||||
r"XPathEvalError",
|
||||
r"xmlXPathEval:",
|
||||
r"XPATH syntax error",
|
||||
r"DOMXPath",
|
||||
r"SimpleXMLElement::xpath\(\)",
|
||||
r"lxml\.etree\.XPathEvalError",
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for XPath injection indicators"""
|
||||
# XPath error messages
|
||||
for pattern in self.error_patterns:
|
||||
match = re.search(pattern, response_body, re.IGNORECASE)
|
||||
if match:
|
||||
return True, 0.85, f"XPath error detected: {match.group(0)[:100]}"
|
||||
|
||||
# Boolean-based XPath injection - true condition returning data
|
||||
if ("' or '1'='1" in payload or "or 1=1" in payload):
|
||||
if response_status == 200:
|
||||
# Check for XML-like data in response
|
||||
xml_tags = re.findall(r"<[a-zA-Z][^>]*>", response_body)
|
||||
if len(xml_tags) > 5:
|
||||
return True, 0.65, "XPath boolean injection may have returned XML data"
|
||||
|
||||
# Check for exposed XML node data
|
||||
if "' | //" in payload or "extractvalue(" in payload.lower():
|
||||
if re.search(r"<\?xml\s+version=", response_body):
|
||||
return True, 0.7, "XML document exposed via XPath injection"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class GraphqlInjectionTester(BaseTester):
|
||||
"""Tester for GraphQL Injection / Introspection vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "graphql_injection"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for GraphQL introspection and injection indicators"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Introspection query response - schema exposure
|
||||
if "__schema" in payload or "__type" in payload or "introspection" in payload.lower():
|
||||
if '"__schema"' in response_body or '"__type"' in response_body:
|
||||
return True, 0.9, "GraphQL introspection enabled - schema exposed"
|
||||
if '"types"' in response_body and '"queryType"' in response_body:
|
||||
return True, 0.9, "GraphQL schema types exposed via introspection"
|
||||
|
||||
# GraphQL error messages revealing structure
|
||||
graphql_errors = [
|
||||
r'"errors"\s*:\s*\[',
|
||||
r"Cannot query field",
|
||||
r"Unknown argument",
|
||||
r"Field .* not found in type",
|
||||
r"Syntax Error.*GraphQL",
|
||||
r"GraphQL error",
|
||||
]
|
||||
for pattern in graphql_errors:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
# Error messages can reveal field/type names
|
||||
if re.search(r'"message"\s*:\s*".*(?:field|type|argument)', response_body, re.IGNORECASE):
|
||||
return True, 0.7, "GraphQL error reveals schema information"
|
||||
|
||||
# Mutation that returned success unexpectedly
|
||||
if "mutation" in payload.lower() and response_status == 200:
|
||||
if '"data"' in response_body and '"errors"' not in response_body:
|
||||
return True, 0.5, "GraphQL mutation succeeded - verify authorization"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class CrlfInjectionTester(BaseTester):
|
||||
"""Tester for CRLF Injection vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "crlf_injection"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for CRLF injection - injected headers appearing in response"""
|
||||
# Check if our injected header appeared in response headers
|
||||
headers_lower = {k.lower(): v for k, v in response_headers.items()}
|
||||
|
||||
if "x-test" in headers_lower:
|
||||
return True, 0.95, f"CRLF injection confirmed: X-Test header injected with value '{headers_lower['x-test']}'"
|
||||
|
||||
if "x-injected" in headers_lower:
|
||||
return True, 0.95, f"CRLF injection confirmed: X-Injected header present"
|
||||
|
||||
# Check for Set-Cookie injection
|
||||
if "set-cookie" in headers_lower and "neurosploit" in str(headers_lower.get("set-cookie", "")).lower():
|
||||
return True, 0.9, "CRLF injection: injected Set-Cookie header detected"
|
||||
|
||||
# Check if payload characters are reflected unencoded in Location header
|
||||
if "location" in headers_lower:
|
||||
location = headers_lower["location"]
|
||||
if "\r\n" in location or "%0d%0a" in location.lower():
|
||||
return True, 0.8, "CRLF characters in Location header"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class HeaderInjectionTester(BaseTester):
|
||||
"""Tester for Host Header Injection vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "header_injection"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for Host header reflected in response URLs"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Check if injected host value appears in response links
|
||||
evil_markers = ["evil.com", "attacker.com", "neurosploit.test"]
|
||||
for marker in evil_markers:
|
||||
if marker in payload.lower():
|
||||
if marker in body_lower:
|
||||
# Check if it appears in URLs, links, or redirects
|
||||
url_pattern = rf'(?:href|src|action|url|link|redirect)\s*[=:]\s*["\']?[^"\']*{re.escape(marker)}'
|
||||
if re.search(url_pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.9, f"Host header injected into response URL: {marker}"
|
||||
return True, 0.7, f"Injected host value '{marker}' reflected in response"
|
||||
|
||||
# Password reset poisoning check
|
||||
if "password" in body_lower and "reset" in body_lower:
|
||||
headers_lower = {k.lower(): v for k, v in response_headers.items()}
|
||||
if response_status in [200, 302]:
|
||||
return True, 0.5, "Password reset response may use Host header for link generation"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class EmailInjectionTester(BaseTester):
|
||||
"""Tester for Email Header Injection vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "email_injection"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for email header injection success indicators"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Check for CC/BCC injection indicators
|
||||
if any(h in payload.lower() for h in ["cc:", "bcc:", "\r\nto:", "%0acc:", "%0abcc:"]):
|
||||
# Successful email send with injected headers
|
||||
if response_status == 200:
|
||||
success_indicators = [
|
||||
"email sent", "message sent", "mail sent",
|
||||
"successfully sent", "email delivered", "sent successfully",
|
||||
]
|
||||
for indicator in success_indicators:
|
||||
if indicator in body_lower:
|
||||
return True, 0.75, f"Email injection: '{indicator}' after CC/BCC injection attempt"
|
||||
|
||||
# Check for multiple recipient confirmation
|
||||
if re.search(r"(?:sent to|delivered to|recipients?)\s*:?\s*\d+", response_body, re.IGNORECASE):
|
||||
return True, 0.7, "Email sent to multiple recipients after injection"
|
||||
|
||||
# SMTP error leak
|
||||
smtp_errors = [r"SMTP error", r"550 \d+", r"relay access denied", r"mail\(\).*failed"]
|
||||
for pattern in smtp_errors:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.6, "SMTP error revealed - email injection attempted"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ELInjectionTester(BaseTester):
|
||||
"""Tester for Expression Language (EL) Injection vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "el_injection"
|
||||
self.math_results = {
|
||||
"${7*7}": "49",
|
||||
"#{7*7}": "49",
|
||||
"${3*11}": "33",
|
||||
"#{3*11}": "33",
|
||||
}
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for EL injection - expression evaluation in response"""
|
||||
# Check mathematical expression results
|
||||
for expr, result in self.math_results.items():
|
||||
if expr in payload and result in response_body:
|
||||
# Make sure the result isn't just the expression echoed
|
||||
if expr not in response_body:
|
||||
return True, 0.95, f"EL injection confirmed: {expr} evaluated to {result}"
|
||||
|
||||
# Java class names exposed via EL
|
||||
java_indicators = [
|
||||
r"java\.lang\.\w+",
|
||||
r"java\.io\.File",
|
||||
r"Runtime\.getRuntime",
|
||||
r"ProcessBuilder",
|
||||
r"javax\.\w+\.\w+",
|
||||
r"org\.apache\.\w+",
|
||||
r"getClass\(\)\.forName",
|
||||
]
|
||||
for pattern in java_indicators:
|
||||
if re.search(pattern, response_body):
|
||||
return True, 0.8, f"Java class exposure via EL injection: {pattern}"
|
||||
|
||||
# Spring EL specific
|
||||
if "T(java.lang" in payload:
|
||||
if re.search(r"class\s+\w+|java\.\w+", response_body):
|
||||
return True, 0.7, "Spring EL injection indicator - Java class reference in response"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class LogInjectionTester(BaseTester):
|
||||
"""Tester for Log Injection / Log4Shell / JNDI Injection vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "log_injection"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for log injection and JNDI callback indicators"""
|
||||
# JNDI/Log4Shell detection via context callback
|
||||
if "${jndi:" in payload.lower():
|
||||
# Check context for callback confirmation
|
||||
if context.get("callback_received"):
|
||||
return True, 0.95, "JNDI injection confirmed via callback"
|
||||
# Check for Log4j error in response
|
||||
log4j_indicators = [
|
||||
r"log4j", r"Log4jException", r"JNDI lookup",
|
||||
r"javax\.naming", r"InitialContext",
|
||||
]
|
||||
for pattern in log4j_indicators:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.7, f"Log4j/JNDI indicator in response: {pattern}"
|
||||
|
||||
# Newline injection in log-like responses
|
||||
if "\n" in payload or "%0a" in payload.lower() or "\\n" in payload:
|
||||
# Check if response includes log-format lines with our injected content
|
||||
log_patterns = [
|
||||
r"\[\d{4}-\d{2}-\d{2}.*\].*neurosploit",
|
||||
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}.*neurosploit",
|
||||
r"(?:INFO|WARN|ERROR|DEBUG)\s+.*neurosploit",
|
||||
]
|
||||
for pattern in log_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.7, "Log injection: injected content appears in log-format output"
|
||||
|
||||
# Generic log forging check
|
||||
if "neurosploit" in payload and response_status == 200:
|
||||
if re.search(r"(?:log|audit|event).*neurosploit", response_body, re.IGNORECASE):
|
||||
return True, 0.5, "Injected marker appears in log/audit output"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class HtmlInjectionTester(BaseTester):
|
||||
"""Tester for HTML Injection vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "html_injection"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for rendered HTML tags in response"""
|
||||
if response_status >= 400:
|
||||
return False, 0.0, None
|
||||
|
||||
# Check for injected HTML tags rendered in response
|
||||
html_tests = [
|
||||
(r"<b>neurosploit</b>", "Bold tag rendered"),
|
||||
(r"<i>neurosploit</i>", "Italic tag rendered"),
|
||||
(r"<u>neurosploit</u>", "Underline tag rendered"),
|
||||
(r'<a\s+href=["\']?[^"\']*["\']?>neurosploit</a>', "Anchor tag rendered"),
|
||||
(r'<img\s+src=["\']?[^"\']*["\']?', "Image tag rendered"),
|
||||
(r'<form\s+[^>]*action=', "Form tag rendered"),
|
||||
(r'<iframe\s+', "IFrame tag rendered"),
|
||||
(r'<marquee>', "Marquee tag rendered"),
|
||||
(r'<h1>neurosploit</h1>', "H1 tag rendered"),
|
||||
(r'<div\s+style=', "Styled div rendered"),
|
||||
]
|
||||
|
||||
for pattern, description in html_tests:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
# Verify it wasn't already there (check if payload was actually injected)
|
||||
if any(tag in payload.lower() for tag in ["<b>", "<i>", "<u>", "<a ", "<img", "<form", "<iframe", "<marquee", "<h1>", "<div"]):
|
||||
return True, 0.8, f"HTML injection: {description}"
|
||||
|
||||
# Check for payload reflection without encoding
|
||||
if "<" in payload and ">" in payload:
|
||||
# Find the injected tag in response
|
||||
tag_match = re.search(r"<(\w+)[^>]*>", payload)
|
||||
if tag_match:
|
||||
tag_name = tag_match.group(1)
|
||||
if f"<{tag_name}" in response_body and f"<{tag_name}" not in response_body:
|
||||
return True, 0.75, f"HTML tag <{tag_name}> reflected without encoding"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class CsvInjectionTester(BaseTester):
|
||||
"""Tester for CSV Injection (Formula Injection) vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "csv_injection"
|
||||
self.formula_chars = ["=", "+", "-", "@", "\t", "\r"]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for formula characters preserved in CSV export responses"""
|
||||
headers_lower = {k.lower(): v.lower() for k, v in response_headers.items()}
|
||||
content_type = headers_lower.get("content-type", "")
|
||||
|
||||
# Check if response is CSV or spreadsheet
|
||||
is_csv = "text/csv" in content_type or "spreadsheet" in content_type
|
||||
is_export = "content-disposition" in headers_lower and any(
|
||||
ext in headers_lower.get("content-disposition", "")
|
||||
for ext in [".csv", ".xls", ".xlsx"]
|
||||
)
|
||||
|
||||
if is_csv or is_export:
|
||||
# Check if formula characters are preserved without escaping
|
||||
for char in self.formula_chars:
|
||||
if char in payload and payload in response_body:
|
||||
return True, 0.8, f"CSV injection: formula character '{char}' preserved in export"
|
||||
|
||||
# Check for specific formula patterns
|
||||
formula_patterns = [
|
||||
r'[=+\-@].*(?:HYPERLINK|IMPORTXML|IMPORTDATA|cmd|powershell)',
|
||||
r'=\w+\(.*\)',
|
||||
]
|
||||
for pattern in formula_patterns:
|
||||
if re.search(pattern, response_body):
|
||||
return True, 0.7, "CSV injection: formula pattern found in export data"
|
||||
|
||||
# Non-CSV response but payload was stored
|
||||
if response_status in [200, 201] and any(c in payload for c in self.formula_chars[:4]):
|
||||
if payload in response_body:
|
||||
return True, 0.4, "Formula characters accepted and stored - verify CSV export"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class OrmInjectionTester(BaseTester):
|
||||
"""Tester for ORM Injection vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "orm_injection"
|
||||
self.error_patterns = [
|
||||
r"Hibernate.*Exception",
|
||||
r"javax\.persistence",
|
||||
r"org\.hibernate",
|
||||
r"ActiveRecord::.*Error",
|
||||
r"Sequelize.*Error",
|
||||
r"SQLAlchemy.*Error",
|
||||
r"Doctrine.*Exception",
|
||||
r"TypeORM.*Error",
|
||||
r"Prisma.*Error",
|
||||
r"EntityFramework.*Exception",
|
||||
r"LINQ.*Exception",
|
||||
r"django\.db.*Error",
|
||||
r"peewee\.\w+Error",
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for ORM injection indicators"""
|
||||
# ORM error messages
|
||||
for pattern in self.error_patterns:
|
||||
match = re.search(pattern, response_body, re.IGNORECASE)
|
||||
if match:
|
||||
return True, 0.8, f"ORM error detected: {match.group(0)[:100]}"
|
||||
|
||||
# Filter manipulation - unexpected data returned
|
||||
if any(op in payload for op in ["__gt", "__lt", "__ne", "$ne", "$gt", ">=", "!="]):
|
||||
if response_status == 200:
|
||||
# Check context for baseline comparison
|
||||
if "baseline_length" in context:
|
||||
diff = abs(len(response_body) - context["baseline_length"])
|
||||
if diff > 500:
|
||||
return True, 0.6, f"ORM filter manipulation: response size differs by {diff} bytes"
|
||||
|
||||
# Check for data volume suggesting bypassed filters
|
||||
if "__all" in payload or "objects.all" in payload:
|
||||
if response_status == 200 and len(response_body) > 10000:
|
||||
return True, 0.5, "ORM injection may have bypassed query filters - large data returned"
|
||||
|
||||
return False, 0.0, None
|
||||
+347
@@ -0,0 +1,347 @@
|
||||
"""
|
||||
NeuroSploit v3 - Authentication Vulnerability Testers
|
||||
|
||||
Testers for Auth Bypass, JWT, Session Fixation
|
||||
"""
|
||||
import re
|
||||
import base64
|
||||
import json
|
||||
from typing import Tuple, Dict, Optional
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
|
||||
class AuthBypassTester(BaseTester):
|
||||
"""Tester for Authentication Bypass"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "auth_bypass"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for authentication bypass"""
|
||||
# Check for successful auth indicators after bypass payload
|
||||
auth_success = [
|
||||
"welcome", "dashboard", "logged in", "authenticated",
|
||||
"success", "admin", "profile"
|
||||
]
|
||||
|
||||
if response_status == 200:
|
||||
body_lower = response_body.lower()
|
||||
for indicator in auth_success:
|
||||
if indicator in body_lower:
|
||||
# Check if this was with a bypass payload
|
||||
bypass_indicators = ["' or '1'='1", "admin'--", "' or 1=1"]
|
||||
if any(bp in payload.lower() for bp in bypass_indicators):
|
||||
return True, 0.8, f"Auth bypass possible: '{indicator}' found after injection"
|
||||
|
||||
# Check for redirect to authenticated area
|
||||
location = response_headers.get("Location", "")
|
||||
if response_status in [301, 302]:
|
||||
if "dashboard" in location or "admin" in location or "home" in location:
|
||||
return True, 0.7, f"Auth bypass: Redirect to {location}"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class JWTManipulationTester(BaseTester):
|
||||
"""Tester for JWT Token Manipulation"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "jwt_manipulation"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for JWT manipulation vulnerabilities"""
|
||||
# Check if manipulated JWT was accepted
|
||||
if response_status == 200:
|
||||
# Algorithm none attack
|
||||
if '"alg":"none"' in payload or '"alg": "none"' in payload:
|
||||
return True, 0.9, "JWT 'none' algorithm accepted"
|
||||
|
||||
# Check for elevated privileges response
|
||||
elevated_indicators = ["admin", "administrator", "role.*admin"]
|
||||
for pattern in elevated_indicators:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.8, "JWT manipulation: Elevated privileges detected"
|
||||
|
||||
# Check for JWT-specific errors
|
||||
jwt_errors = [
|
||||
r"invalid.*token", r"jwt.*expired", r"signature.*invalid",
|
||||
r"token.*malformed", r"unauthorized"
|
||||
]
|
||||
for pattern in jwt_errors:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
# Error means it's checking - note for further testing
|
||||
return False, 0.0, None
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class SessionFixationTester(BaseTester):
|
||||
"""Tester for Session Fixation"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "session_fixation"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for session fixation vulnerability"""
|
||||
# Check Set-Cookie header
|
||||
set_cookie = response_headers.get("Set-Cookie", "")
|
||||
|
||||
# If session ID in URL was accepted
|
||||
if "JSESSIONID=" in payload or "PHPSESSID=" in payload:
|
||||
if response_status == 200:
|
||||
# Check if session was NOT regenerated
|
||||
if not set_cookie or "JSESSIONID" not in set_cookie:
|
||||
return True, 0.7, "Session ID from URL accepted without regeneration"
|
||||
|
||||
# Check for session in URL
|
||||
if re.search(r'[?&](?:session|sid|PHPSESSID|JSESSIONID)=', response_body):
|
||||
return True, 0.6, "Session ID exposed in URL"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class WeakPasswordTester(BaseTester):
|
||||
"""Tester for Weak Password acceptance"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "weak_password"
|
||||
self.weak_passwords = [
|
||||
"123456", "password", "12345678", "qwerty", "abc123",
|
||||
"111111", "123123", "admin", "letmein", "welcome",
|
||||
"1234", "1", "a"
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for successful login/registration with weak passwords"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Check if payload contains a weak password
|
||||
payload_has_weak = any(wp in payload for wp in self.weak_passwords)
|
||||
if not payload_has_weak:
|
||||
return False, 0.0, None
|
||||
|
||||
# Check for successful auth with weak password
|
||||
if response_status in [200, 201, 302]:
|
||||
success_indicators = [
|
||||
r'"(?:access_)?token"\s*:', r'"session"\s*:',
|
||||
r"(?:login|registration|signup)\s+successful",
|
||||
r'"authenticated"\s*:\s*true', r'"success"\s*:\s*true',
|
||||
r"welcome", r"dashboard", r"logged\s*in",
|
||||
]
|
||||
for pattern in success_indicators:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
matched_pw = next((wp for wp in self.weak_passwords if wp in payload), "unknown")
|
||||
return True, 0.85, f"Weak password accepted: '{matched_pw}' allowed for authentication"
|
||||
|
||||
# Redirect to authenticated area
|
||||
location = response_headers.get("Location", "")
|
||||
if response_status == 302 and any(x in location.lower() for x in ["dashboard", "home", "profile", "account"]):
|
||||
matched_pw = next((wp for wp in self.weak_passwords if wp in payload), "unknown")
|
||||
return True, 0.8, f"Weak password accepted: Redirect to authenticated area with '{matched_pw}'"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class DefaultCredentialsTester(BaseTester):
|
||||
"""Tester for Default Credentials acceptance"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "default_credentials"
|
||||
self.default_creds = [
|
||||
("admin", "admin"), ("admin", "password"), ("admin", "admin123"),
|
||||
("root", "root"), ("root", "toor"), ("root", "password"),
|
||||
("administrator", "administrator"), ("admin", "1234"),
|
||||
("test", "test"), ("guest", "guest"), ("user", "user"),
|
||||
("admin", "changeme"), ("admin", "default"),
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for successful login with default credentials"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Check if payload matches default creds
|
||||
payload_lower = payload.lower()
|
||||
matched_cred = None
|
||||
for username, password in self.default_creds:
|
||||
if username in payload_lower and password in payload_lower:
|
||||
matched_cred = f"{username}/{password}"
|
||||
break
|
||||
|
||||
if not matched_cred:
|
||||
return False, 0.0, None
|
||||
|
||||
# Check for successful login
|
||||
if response_status in [200, 201, 302]:
|
||||
auth_success = [
|
||||
r'"(?:access_)?token"\s*:', r'"session"\s*:',
|
||||
r"(?:login|auth)\s+successful", r'"success"\s*:\s*true',
|
||||
r'"authenticated"\s*:\s*true', r"welcome",
|
||||
r"dashboard", r"admin\s*panel",
|
||||
]
|
||||
for pattern in auth_success:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.9, f"Default credentials accepted: {matched_cred}"
|
||||
|
||||
# Redirect to admin/dashboard
|
||||
location = response_headers.get("Location", "")
|
||||
if response_status == 302 and any(x in location.lower() for x in ["dashboard", "admin", "home", "panel"]):
|
||||
return True, 0.85, f"Default credentials accepted: {matched_cred} (redirect to {location})"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class TwoFactorBypassTester(BaseTester):
|
||||
"""Tester for Two-Factor Authentication Bypass"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "two_factor_bypass"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for authenticated access without completing 2FA"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Check if we reached authenticated content without 2FA
|
||||
if response_status == 200:
|
||||
# Authenticated area indicators
|
||||
auth_area_patterns = [
|
||||
r"dashboard", r"my\s*account", r"profile",
|
||||
r"settings", r"admin\s*panel", r'"user"\s*:\s*\{',
|
||||
r'"email"\s*:\s*"[^"]+"', r'"role"\s*:',
|
||||
]
|
||||
# 2FA page indicators (we should NOT see these if bypassed)
|
||||
twofa_page_patterns = [
|
||||
r"(?:enter|verify)\s+(?:your\s+)?(?:otp|code|token|2fa)",
|
||||
r"two.?factor", r"verification\s+code",
|
||||
r"authenticator", r"sms\s+code",
|
||||
]
|
||||
|
||||
has_auth_content = any(re.search(p, response_body, re.IGNORECASE) for p in auth_area_patterns)
|
||||
is_twofa_page = any(re.search(p, response_body, re.IGNORECASE) for p in twofa_page_patterns)
|
||||
|
||||
if has_auth_content and not is_twofa_page:
|
||||
# Check if payload suggests 2FA bypass attempt
|
||||
bypass_indicators = [
|
||||
"2fa", "otp", "mfa", "verify", "code",
|
||||
"step2", "second", "challenge",
|
||||
]
|
||||
if any(bi in payload.lower() for bi in bypass_indicators):
|
||||
return True, 0.85, "2FA bypass: Authenticated area accessed without completing 2FA"
|
||||
|
||||
# Direct navigation bypass
|
||||
if context.get("skip_2fa") or context.get("direct_access"):
|
||||
return True, 0.9, "2FA bypass: Direct navigation to authenticated page bypassed 2FA"
|
||||
|
||||
# Redirect skipping 2FA step
|
||||
if response_status in [301, 302]:
|
||||
location = response_headers.get("Location", "").lower()
|
||||
if any(x in location for x in ["dashboard", "home", "account"]):
|
||||
if "verify" not in location and "2fa" not in location and "otp" not in location:
|
||||
return True, 0.7, "2FA bypass: Redirect to authenticated area skipping verification"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class OauthMisconfigTester(BaseTester):
|
||||
"""Tester for OAuth Misconfiguration"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "oauth_misconfig"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for OAuth misconfiguration (open redirect, token leakage)"""
|
||||
# Check for open redirect in OAuth flow
|
||||
if response_status in [301, 302, 303, 307]:
|
||||
location = response_headers.get("Location", "")
|
||||
|
||||
# External redirect in OAuth callback
|
||||
if "redirect_uri" in payload.lower() or "callback" in payload.lower():
|
||||
# Check if redirecting to attacker-controlled domain
|
||||
evil_domains = ["evil.com", "attacker.com", "malicious.com"]
|
||||
if any(domain in location for domain in evil_domains):
|
||||
return True, 0.9, f"OAuth misconfig: Open redirect in OAuth flow to {location}"
|
||||
|
||||
# Check if arbitrary redirect_uri accepted
|
||||
if payload in location:
|
||||
return True, 0.85, "OAuth misconfig: Arbitrary redirect_uri accepted"
|
||||
|
||||
# Check for token in URL parameters (should be in fragment or POST)
|
||||
if response_status in [200, 302]:
|
||||
location = response_headers.get("Location", "")
|
||||
# Token in query string instead of fragment
|
||||
token_in_url = re.search(
|
||||
r'[?&](?:access_token|token|code)=([A-Za-z0-9._-]+)',
|
||||
location
|
||||
)
|
||||
if token_in_url:
|
||||
return True, 0.8, "OAuth misconfig: Token/code exposed in URL query parameters"
|
||||
|
||||
# Token in response body URL
|
||||
token_in_body = re.search(
|
||||
r'(?:redirect|callback|return)["\']?\s*[:=]\s*["\']?https?://[^"\'>\s]*[?&]access_token=',
|
||||
response_body, re.IGNORECASE
|
||||
)
|
||||
if token_in_body:
|
||||
return True, 0.75, "OAuth misconfig: Access token in redirect URL"
|
||||
|
||||
# Check for missing state parameter (CSRF in OAuth)
|
||||
if "state=" not in response_body and "state=" not in response_headers.get("Location", ""):
|
||||
if re.search(r"(?:authorize|oauth|auth)\?", response_body, re.IGNORECASE):
|
||||
return True, 0.6, "OAuth misconfig: Missing state parameter (CSRF risk)"
|
||||
|
||||
return False, 0.0, None
|
||||
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
NeuroSploit v3 - Authorization Vulnerability Testers
|
||||
|
||||
Testers for IDOR, BOLA, Privilege Escalation
|
||||
"""
|
||||
import re
|
||||
from typing import Tuple, Dict, Optional
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
|
||||
class IDORTester(BaseTester):
|
||||
"""Tester for Insecure Direct Object Reference"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "idor"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for IDOR vulnerability"""
|
||||
# Check if we got data for a different ID
|
||||
if response_status == 200:
|
||||
# Look for user data indicators
|
||||
user_data_patterns = [
|
||||
r'"user_?id"\s*:\s*\d+',
|
||||
r'"email"\s*:\s*"[^"]+"',
|
||||
r'"name"\s*:\s*"[^"]+"',
|
||||
r'"account"\s*:',
|
||||
r'"profile"\s*:'
|
||||
]
|
||||
|
||||
for pattern in user_data_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
# Check if ID in payload differs from context user
|
||||
if "original_id" in context:
|
||||
if context["original_id"] not in payload:
|
||||
return True, 0.8, f"IDOR: Accessed different user's data"
|
||||
|
||||
# Generic data access check
|
||||
if len(response_body) > 50:
|
||||
return True, 0.6, "IDOR: Response contains data - verify authorization"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class BOLATester(BaseTester):
|
||||
"""Tester for Broken Object Level Authorization"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "bola"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for BOLA in APIs"""
|
||||
# BOLA in REST APIs
|
||||
if response_status == 200:
|
||||
# Check for successful data access
|
||||
data_indicators = [
|
||||
r'"data"\s*:\s*\{',
|
||||
r'"items"\s*:\s*\[',
|
||||
r'"result"\s*:\s*\{',
|
||||
r'"id"\s*:\s*\d+'
|
||||
]
|
||||
|
||||
for pattern in data_indicators:
|
||||
if re.search(pattern, response_body):
|
||||
return True, 0.7, "BOLA: API returned object data - verify authorization"
|
||||
|
||||
# Check for enumeration possibilities
|
||||
if response_status in [200, 404]:
|
||||
# Different status for valid vs invalid IDs indicates BOLA risk
|
||||
return True, 0.5, "BOLA: Different responses for IDs - enumeration possible"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class PrivilegeEscalationTester(BaseTester):
|
||||
"""Tester for Privilege Escalation"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "privilege_escalation"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for privilege escalation"""
|
||||
if response_status == 200:
|
||||
# Check for admin/elevated access indicators
|
||||
elevated_access = [
|
||||
r'"role"\s*:\s*"admin"',
|
||||
r'"is_?admin"\s*:\s*true',
|
||||
r'"admin"\s*:\s*true',
|
||||
r'"privilege"\s*:\s*"(?:admin|root|superuser)"',
|
||||
r'"permissions"\s*:\s*\[.*"admin".*\]'
|
||||
]
|
||||
|
||||
for pattern in elevated_access:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.9, f"Privilege escalation: Elevated role in response"
|
||||
|
||||
# Check for admin functionality access
|
||||
admin_functions = [
|
||||
"user management", "delete user", "admin panel",
|
||||
"system settings", "all users", "user list"
|
||||
]
|
||||
body_lower = response_body.lower()
|
||||
for func in admin_functions:
|
||||
if func in body_lower:
|
||||
return True, 0.7, f"Privilege escalation: Admin functionality '{func}' accessible"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class BflaTester(BaseTester):
|
||||
"""Tester for Broken Function Level Authorization (BFLA)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "bfla"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for admin functionality accessible to regular user"""
|
||||
if response_status == 200:
|
||||
# Admin-specific data patterns
|
||||
admin_data_patterns = [
|
||||
r'"users"\s*:\s*\[',
|
||||
r'"all_users"\s*:',
|
||||
r'"admin_settings"\s*:',
|
||||
r'"system_config"\s*:',
|
||||
r'"audit_log"\s*:',
|
||||
r'"role"\s*:\s*"admin"',
|
||||
r'"permissions"\s*:\s*\[',
|
||||
r'"api_keys"\s*:\s*\[',
|
||||
]
|
||||
|
||||
for pattern in admin_data_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
# Check if this was an admin endpoint accessed as regular user
|
||||
admin_url_indicators = [
|
||||
"/admin", "/manage", "/users", "/settings",
|
||||
"/config", "/system", "/audit", "/logs",
|
||||
]
|
||||
if any(ind in payload.lower() for ind in admin_url_indicators):
|
||||
return True, 0.85, f"BFLA: Admin data returned for non-admin request"
|
||||
|
||||
# Admin page content
|
||||
admin_content = [
|
||||
"user management", "system configuration", "admin dashboard",
|
||||
"manage users", "all accounts", "server status",
|
||||
"delete user", "create admin",
|
||||
]
|
||||
body_lower = response_body.lower()
|
||||
for content in admin_content:
|
||||
if content in body_lower:
|
||||
return True, 0.8, f"BFLA: Admin functionality '{content}' accessible to regular user"
|
||||
|
||||
# Admin endpoint should return 403 for non-admin
|
||||
if response_status not in [401, 403] and context.get("is_admin_endpoint"):
|
||||
if response_status == 200:
|
||||
return True, 0.7, "BFLA: Admin endpoint returned 200 instead of 403"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class MassAssignmentTester(BaseTester):
|
||||
"""Tester for Mass Assignment vulnerability"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "mass_assignment"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for extra parameters being accepted and modifying model state"""
|
||||
if response_status in [200, 201]:
|
||||
# Check if privileged fields were accepted
|
||||
privileged_fields = [
|
||||
(r'"(?:is_)?admin"\s*:\s*true', "admin flag"),
|
||||
(r'"role"\s*:\s*"admin"', "admin role"),
|
||||
(r'"role"\s*:\s*"superuser"', "superuser role"),
|
||||
(r'"verified"\s*:\s*true', "verified status"),
|
||||
(r'"is_staff"\s*:\s*true', "staff flag"),
|
||||
(r'"is_superuser"\s*:\s*true', "superuser flag"),
|
||||
(r'"balance"\s*:\s*\d{4,}', "balance modification"),
|
||||
(r'"credits"\s*:\s*\d{3,}', "credits modification"),
|
||||
(r'"discount"\s*:\s*\d+', "discount field"),
|
||||
(r'"price"\s*:\s*0', "price zeroed"),
|
||||
]
|
||||
|
||||
# Check if payload attempted mass assignment
|
||||
mass_assign_indicators = [
|
||||
"admin", "role", "is_staff", "is_superuser",
|
||||
"verified", "balance", "credits", "price", "discount",
|
||||
]
|
||||
payload_has_mass_assign = any(ind in payload.lower() for ind in mass_assign_indicators)
|
||||
|
||||
if payload_has_mass_assign:
|
||||
for pattern, field_name in privileged_fields:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.85, f"Mass assignment: Privileged field '{field_name}' accepted and reflected"
|
||||
|
||||
# Check if response changed compared to baseline
|
||||
if context.get("baseline_body"):
|
||||
baseline = context["baseline_body"]
|
||||
if response_body != baseline and len(response_body) > len(baseline):
|
||||
return True, 0.6, "Mass assignment: Response differs from baseline after extra parameters"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ForcedBrowsingTester(BaseTester):
|
||||
"""Tester for Forced Browsing (direct access to restricted URLs)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "forced_browsing"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for direct access to restricted URLs without proper authorization"""
|
||||
# Should get 401/403/302 for restricted content, not 200
|
||||
if response_status == 200:
|
||||
# Sensitive content indicators
|
||||
sensitive_patterns = [
|
||||
(r"(?:admin|management)\s+(?:panel|dashboard|console)", "admin panel"),
|
||||
(r'"(?:password|secret|api_key|token)"\s*:\s*"[^"]+"', "sensitive data"),
|
||||
(r"(?:backup|dump|export)\s+(?:file|data|database)", "backup files"),
|
||||
(r"phpinfo\(\)", "PHP info page"),
|
||||
(r"(?:configuration|config)\s+(?:file|settings)", "configuration page"),
|
||||
(r"(?:internal|private)\s+(?:api|endpoint|documentation)", "internal docs"),
|
||||
(r"(?:debug|diagnostic)\s+(?:info|page|console)", "debug page"),
|
||||
(r"(?:user|customer)\s+(?:list|database|records)", "user records"),
|
||||
]
|
||||
|
||||
for pattern, desc in sensitive_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.8, f"Forced browsing: Restricted content accessible - {desc}"
|
||||
|
||||
# Check for restricted URL patterns in payload
|
||||
restricted_paths = [
|
||||
"/admin", "/backup", "/config", "/internal",
|
||||
"/debug", "/private", "/management", "/phpinfo",
|
||||
"/.git", "/.env", "/wp-admin", "/server-status",
|
||||
]
|
||||
if any(path in payload.lower() for path in restricted_paths):
|
||||
if len(response_body) > 200:
|
||||
return True, 0.7, "Forced browsing: Restricted URL returned content (200 OK)"
|
||||
|
||||
# Check that redirect isn't to the same restricted page (false positive)
|
||||
if response_status in [301, 302]:
|
||||
location = response_headers.get("Location", "").lower()
|
||||
if "login" in location or "signin" in location or "auth" in location:
|
||||
return False, 0.0, None # Properly redirecting to login
|
||||
|
||||
return False, 0.0, None
|
||||
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
NeuroSploit v3 - Base Vulnerability Tester
|
||||
|
||||
Base class for all vulnerability testers.
|
||||
"""
|
||||
from typing import Tuple, Dict, List, Optional, Any
|
||||
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
|
||||
|
||||
|
||||
class BaseTester:
|
||||
"""Base class for vulnerability testers"""
|
||||
|
||||
def __init__(self):
|
||||
self.name = "base"
|
||||
|
||||
def build_request(
|
||||
self,
|
||||
endpoint,
|
||||
payload: str
|
||||
) -> Tuple[str, Dict, Dict, Optional[str]]:
|
||||
"""
|
||||
Build a test request with the payload.
|
||||
|
||||
Returns:
|
||||
Tuple of (url, params, headers, body)
|
||||
"""
|
||||
url = endpoint.url
|
||||
params = {}
|
||||
headers = {"User-Agent": "NeuroSploit/3.0"}
|
||||
body = None
|
||||
|
||||
# Inject payload into parameters if endpoint has them
|
||||
if endpoint.parameters:
|
||||
for param in endpoint.parameters:
|
||||
param_name = param.get("name", param) if isinstance(param, dict) else param
|
||||
params[param_name] = payload
|
||||
else:
|
||||
# Try to inject into URL query string
|
||||
parsed = urlparse(url)
|
||||
if parsed.query:
|
||||
query_params = parse_qs(parsed.query)
|
||||
for key in query_params:
|
||||
query_params[key] = [payload]
|
||||
new_query = urlencode(query_params, doseq=True)
|
||||
url = urlunparse(parsed._replace(query=new_query))
|
||||
else:
|
||||
# Add as query parameter
|
||||
params["test"] = payload
|
||||
|
||||
return url, params, headers, body
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""
|
||||
Analyze response to determine if vulnerable.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_vulnerable, confidence, evidence)
|
||||
"""
|
||||
return False, 0.0, None
|
||||
|
||||
def check_timeout_vulnerability(self, vuln_type: str) -> bool:
|
||||
"""Check if timeout indicates vulnerability for this type"""
|
||||
return False
|
||||
|
||||
def get_injection_points(self, endpoint) -> List[Dict]:
|
||||
"""Get all injection points for an endpoint"""
|
||||
points = []
|
||||
|
||||
# URL parameters
|
||||
if endpoint.parameters:
|
||||
for param in endpoint.parameters:
|
||||
param_name = param.get("name", param) if isinstance(param, dict) else param
|
||||
points.append({
|
||||
"type": "parameter",
|
||||
"name": param_name,
|
||||
"location": "query"
|
||||
})
|
||||
|
||||
# Parse URL for query params
|
||||
parsed = urlparse(endpoint.url)
|
||||
if parsed.query:
|
||||
query_params = parse_qs(parsed.query)
|
||||
for key in query_params:
|
||||
if not any(p.get("name") == key for p in points):
|
||||
points.append({
|
||||
"type": "parameter",
|
||||
"name": key,
|
||||
"location": "query"
|
||||
})
|
||||
|
||||
# Headers that might be injectable
|
||||
injectable_headers = ["User-Agent", "Referer", "X-Forwarded-For", "Cookie"]
|
||||
for header in injectable_headers:
|
||||
points.append({
|
||||
"type": "header",
|
||||
"name": header,
|
||||
"location": "header"
|
||||
})
|
||||
|
||||
return points
|
||||
@@ -0,0 +1,430 @@
|
||||
"""
|
||||
NeuroSploit v3 - Client-Side Vulnerability Testers
|
||||
|
||||
Testers for CORS, Clickjacking, Open Redirect
|
||||
"""
|
||||
import re
|
||||
from typing import Tuple, Dict, Optional
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
|
||||
class CORSTester(BaseTester):
|
||||
"""Tester for CORS Misconfiguration"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "cors_misconfig"
|
||||
|
||||
def build_request(self, endpoint, payload: str) -> Tuple[str, Dict, Dict, Optional[str]]:
|
||||
"""Build CORS test request with Origin header"""
|
||||
headers = {
|
||||
"User-Agent": "NeuroSploit/3.0",
|
||||
"Origin": payload # payload is the test origin
|
||||
}
|
||||
return endpoint.url, {}, headers, None
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for CORS misconfiguration"""
|
||||
acao = response_headers.get("Access-Control-Allow-Origin", "")
|
||||
acac = response_headers.get("Access-Control-Allow-Credentials", "")
|
||||
|
||||
# Wildcard with credentials
|
||||
if acao == "*" and acac.lower() == "true":
|
||||
return True, 0.95, "CORS: Wildcard origin with credentials allowed"
|
||||
|
||||
# Origin reflection
|
||||
if acao == payload:
|
||||
if acac.lower() == "true":
|
||||
return True, 0.9, f"CORS: Arbitrary origin '{payload}' reflected with credentials"
|
||||
return True, 0.7, f"CORS: Arbitrary origin '{payload}' reflected"
|
||||
|
||||
# Wildcard (without credentials still risky)
|
||||
if acao == "*":
|
||||
return True, 0.5, "CORS: Wildcard origin allowed"
|
||||
|
||||
# Null origin accepted
|
||||
if acao == "null":
|
||||
return True, 0.8, "CORS: Null origin accepted"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ClickjackingTester(BaseTester):
|
||||
"""Tester for Clickjacking vulnerability"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "clickjacking"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for clickjacking protection"""
|
||||
# Check X-Frame-Options
|
||||
xfo = response_headers.get("X-Frame-Options", "").upper()
|
||||
|
||||
# Check CSP frame-ancestors
|
||||
csp = response_headers.get("Content-Security-Policy", "")
|
||||
has_frame_ancestors = "frame-ancestors" in csp.lower()
|
||||
|
||||
if not xfo and not has_frame_ancestors:
|
||||
return True, 0.8, "Clickjacking: No X-Frame-Options or frame-ancestors CSP"
|
||||
|
||||
if xfo and xfo not in ["DENY", "SAMEORIGIN"]:
|
||||
return True, 0.7, f"Clickjacking: Weak X-Frame-Options: {xfo}"
|
||||
|
||||
# Check for JS frame busting that can be bypassed
|
||||
frame_busters = [
|
||||
r"if\s*\(\s*top\s*[!=]=",
|
||||
r"if\s*\(\s*self\s*[!=]=\s*top",
|
||||
r"if\s*\(\s*parent\s*[!=]="
|
||||
]
|
||||
for pattern in frame_busters:
|
||||
if re.search(pattern, response_body):
|
||||
if not xfo and not has_frame_ancestors:
|
||||
return True, 0.6, "Clickjacking: Only JS frame-busting (bypassable)"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class OpenRedirectTester(BaseTester):
|
||||
"""Tester for Open Redirect vulnerability"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "open_redirect"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for open redirect"""
|
||||
# Check redirect status and Location header
|
||||
if response_status in [301, 302, 303, 307, 308]:
|
||||
location = response_headers.get("Location", "")
|
||||
|
||||
# Check if our payload URL is in Location
|
||||
if payload in location:
|
||||
return True, 0.9, f"Open redirect: Redirecting to {location}"
|
||||
|
||||
# Check for partial match (domain)
|
||||
if "evil.com" in payload and "evil.com" in location:
|
||||
return True, 0.9, "Open redirect: External domain in redirect"
|
||||
|
||||
# Check for meta refresh redirect
|
||||
meta_refresh = re.search(
|
||||
r'<meta[^>]+http-equiv=["\']?refresh["\']?[^>]+content=["\']?\d+;\s*url=([^"\'>\s]+)',
|
||||
response_body, re.IGNORECASE
|
||||
)
|
||||
if meta_refresh:
|
||||
redirect_url = meta_refresh.group(1)
|
||||
if payload in redirect_url:
|
||||
return True, 0.8, f"Open redirect via meta refresh: {redirect_url}"
|
||||
|
||||
# Check for JavaScript redirect
|
||||
js_redirects = [
|
||||
rf'location\.href\s*=\s*["\']?{re.escape(payload)}',
|
||||
rf'location\.assign\s*\(["\']?{re.escape(payload)}',
|
||||
rf'location\.replace\s*\(["\']?{re.escape(payload)}'
|
||||
]
|
||||
for pattern in js_redirects:
|
||||
if re.search(pattern, response_body):
|
||||
return True, 0.7, "Open redirect via JavaScript"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class DomClobberingTester(BaseTester):
|
||||
"""Tester for DOM Clobbering vulnerability"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "dom_clobbering"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for HTML injection that could override JS variables via DOM clobbering"""
|
||||
if response_status == 200:
|
||||
# Check if injected HTML with id/name attributes is reflected
|
||||
clobber_patterns = [
|
||||
r'<(?:a|form|img|input|iframe|embed|object)\s+[^>]*(?:id|name)\s*=\s*["\']?(?:' + re.escape(payload.split("=")[0] if "=" in payload else payload) + r')',
|
||||
r'<a\s+[^>]*id=["\'][^"\']+["\'][^>]*href=["\']',
|
||||
r'<form\s+[^>]*name=["\'][^"\']+["\']',
|
||||
]
|
||||
for pattern in clobber_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.8, "DOM Clobbering: Injected HTML with id/name attribute reflected"
|
||||
|
||||
# Check for common clobberable global variables
|
||||
clobber_targets = [
|
||||
r'<[^>]+id=["\'](?:location|document|window|self|top|frames|opener|parent)["\']',
|
||||
r'<[^>]+name=["\'](?:location|document|window|self|top|frames|opener|parent)["\']',
|
||||
]
|
||||
for pattern in clobber_targets:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.85, "DOM Clobbering: HTML element with JS global variable name injected"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class PostMessageVulnTester(BaseTester):
|
||||
"""Tester for postMessage vulnerability (missing origin check)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "postmessage_vuln"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for addEventListener('message') without origin validation"""
|
||||
if response_status == 200:
|
||||
# Find message event listeners
|
||||
message_listener = re.search(
|
||||
r'addEventListener\s*\(\s*["\']message["\']',
|
||||
response_body
|
||||
)
|
||||
if message_listener:
|
||||
# Check if origin is NOT validated nearby
|
||||
# Get surrounding context (500 chars after listener)
|
||||
listener_pos = message_listener.start()
|
||||
handler_block = response_body[listener_pos:listener_pos + 500]
|
||||
|
||||
origin_checks = [
|
||||
r'\.origin\s*[!=]==?\s*["\']',
|
||||
r'event\.origin',
|
||||
r'e\.origin',
|
||||
r'msg\.origin',
|
||||
r'origin\s*===',
|
||||
]
|
||||
has_origin_check = any(re.search(p, handler_block) for p in origin_checks)
|
||||
|
||||
if not has_origin_check:
|
||||
return True, 0.85, "postMessage vulnerability: Message listener without origin validation"
|
||||
else:
|
||||
# Origin check exists but might be weak
|
||||
if re.search(r'\.origin\s*[!=]==?\s*["\']["\']', handler_block):
|
||||
return True, 0.7, "postMessage vulnerability: Origin check appears to be empty string"
|
||||
|
||||
# Check for postMessage with wildcard origin
|
||||
wildcard_post = re.search(
|
||||
r'\.postMessage\s*\([^)]+,\s*["\']\*["\']',
|
||||
response_body
|
||||
)
|
||||
if wildcard_post:
|
||||
return True, 0.75, "postMessage vulnerability: postMessage with wildcard '*' target origin"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class WebsocketHijackTester(BaseTester):
|
||||
"""Tester for WebSocket Cross-Origin Hijacking"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "websocket_hijack"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for WebSocket connections accepting cross-origin requests"""
|
||||
# WebSocket upgrade accepted (101 Switching Protocols)
|
||||
if response_status == 101:
|
||||
# Check if Origin header was sent and accepted
|
||||
upgrade = response_headers.get("Upgrade", "").lower()
|
||||
if upgrade == "websocket":
|
||||
return True, 0.8, "WebSocket hijack: Cross-origin WebSocket upgrade accepted"
|
||||
|
||||
# Check for WebSocket endpoint in response without origin validation
|
||||
if response_status == 200:
|
||||
ws_patterns = [
|
||||
r'new\s+WebSocket\s*\(\s*["\']wss?://',
|
||||
r'ws://[^"\'>\s]+',
|
||||
r'wss://[^"\'>\s]+',
|
||||
]
|
||||
for pattern in ws_patterns:
|
||||
if re.search(pattern, response_body):
|
||||
# Check for lack of CORS-like origin checking
|
||||
if "origin" not in response_body.lower() or context.get("cross_origin_accepted"):
|
||||
return True, 0.7, "WebSocket hijack: WebSocket endpoint found without apparent origin validation"
|
||||
|
||||
# 403 on WebSocket with wrong origin is good (not vulnerable)
|
||||
if response_status == 403:
|
||||
return False, 0.0, None
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class PrototypePollutionTester(BaseTester):
|
||||
"""Tester for JavaScript Prototype Pollution"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "prototype_pollution"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for __proto__ pollution indicators"""
|
||||
if response_status == 200:
|
||||
# Check if __proto__ payload was processed
|
||||
proto_indicators = [
|
||||
r'__proto__',
|
||||
r'constructor\.prototype',
|
||||
r'Object\.prototype',
|
||||
]
|
||||
|
||||
# Payload should contain proto pollution attempt
|
||||
is_proto_payload = any(
|
||||
ind in payload for ind in ["__proto__", "constructor", "prototype"]
|
||||
)
|
||||
|
||||
if is_proto_payload:
|
||||
# Check for pollution effect in response
|
||||
pollution_effects = [
|
||||
r'"__proto__"\s*:\s*\{',
|
||||
r'"polluted"\s*:\s*true',
|
||||
r'"isAdmin"\s*:\s*true',
|
||||
r'"__proto__":\s*\{[^}]*\}',
|
||||
]
|
||||
for pattern in pollution_effects:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.85, "Prototype pollution: __proto__ property accepted and reflected"
|
||||
|
||||
# Check if server processed the prototype chain modification
|
||||
if response_status == 200 and "__proto__" in response_body:
|
||||
return True, 0.7, "Prototype pollution: __proto__ present in server response"
|
||||
|
||||
# Check for error indicating proto processing
|
||||
if re.search(r"(?:cannot|unable to).*(?:merge|assign|extend).*proto", response_body, re.IGNORECASE):
|
||||
return True, 0.6, "Prototype pollution: Server attempted to process __proto__"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class CssInjectionTester(BaseTester):
|
||||
"""Tester for CSS Injection vulnerability"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "css_injection"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for CSS code rendered in style context"""
|
||||
if response_status == 200:
|
||||
# Check if CSS payload is reflected in style context
|
||||
css_contexts = [
|
||||
# Inside <style> tags
|
||||
r'<style[^>]*>(?:[^<]*?)' + re.escape(payload)[:30].replace("\\", "\\\\"),
|
||||
# Inside style attribute
|
||||
r'style\s*=\s*["\'][^"\']*' + re.escape(payload)[:30].replace("\\", "\\\\"),
|
||||
]
|
||||
|
||||
for pattern in css_contexts:
|
||||
try:
|
||||
if re.search(pattern, response_body, re.IGNORECASE | re.DOTALL):
|
||||
return True, 0.85, "CSS injection: Payload reflected in style context"
|
||||
except re.error:
|
||||
continue
|
||||
|
||||
# Check for common CSS injection payloads reflected
|
||||
css_attack_patterns = [
|
||||
r'expression\s*\(',
|
||||
r'url\s*\(\s*["\']?javascript:',
|
||||
r'@import\s+["\']?https?://',
|
||||
r'background:\s*url\s*\(\s*["\']?https?://[^"\')\s]*attacker',
|
||||
r'behavior:\s*url\s*\(',
|
||||
r'-moz-binding:\s*url\s*\(',
|
||||
]
|
||||
for pattern in css_attack_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.8, "CSS injection: Dangerous CSS property reflected"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class TabnabbingTester(BaseTester):
|
||||
"""Tester for Reverse Tabnabbing vulnerability"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "tabnabbing"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for target=_blank links without rel=noopener"""
|
||||
if response_status == 200:
|
||||
# Find all target=_blank links
|
||||
blank_links = re.finditer(
|
||||
r'<a\s+[^>]*target\s*=\s*["\']_blank["\'][^>]*>',
|
||||
response_body,
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
vulnerable_count = 0
|
||||
for match in blank_links:
|
||||
link_tag = match.group(0)
|
||||
# Check for rel=noopener or rel=noreferrer
|
||||
has_protection = re.search(
|
||||
r'rel\s*=\s*["\'][^"\']*(?:noopener|noreferrer)[^"\']*["\']',
|
||||
link_tag,
|
||||
re.IGNORECASE
|
||||
)
|
||||
if not has_protection:
|
||||
vulnerable_count += 1
|
||||
|
||||
if vulnerable_count > 0:
|
||||
confidence = min(0.5 + vulnerable_count * 0.1, 0.8)
|
||||
return True, confidence, f"Tabnabbing: {vulnerable_count} target=_blank link(s) without rel=noopener/noreferrer"
|
||||
|
||||
return False, 0.0, None
|
||||
@@ -0,0 +1,405 @@
|
||||
"""
|
||||
NeuroSploit v3 - Cloud & Supply Chain Vulnerability Testers
|
||||
|
||||
Testers for S3 misconfiguration, cloud metadata, subdomain takeover,
|
||||
vulnerable dependencies, container escape, and serverless misconfiguration.
|
||||
"""
|
||||
import re
|
||||
from typing import Tuple, Dict, Optional
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
|
||||
class S3BucketMisconfigTester(BaseTester):
|
||||
"""Tester for S3 Bucket Misconfiguration vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "s3_bucket_misconfig"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for S3 bucket listing or misconfiguration"""
|
||||
# Bucket listing XML response
|
||||
if re.search(r"<ListBucketResult\s", response_body):
|
||||
return True, 0.95, "S3 bucket listing enabled - bucket contents exposed"
|
||||
|
||||
# Bucket listing with objects
|
||||
if "<Contents>" in response_body and "<Key>" in response_body:
|
||||
keys = re.findall(r"<Key>([^<]+)</Key>", response_body)
|
||||
if keys:
|
||||
return True, 0.95, f"S3 bucket listing: {len(keys)} objects exposed (e.g., {keys[0][:50]})"
|
||||
|
||||
# NoSuchBucket - potential subdomain takeover
|
||||
if "NoSuchBucket" in response_body:
|
||||
bucket_match = re.search(r"<BucketName>([^<]+)</BucketName>", response_body)
|
||||
bucket_name = bucket_match.group(1) if bucket_match else "unknown"
|
||||
return True, 0.8, f"S3 bucket '{bucket_name}' does not exist - potential takeover"
|
||||
|
||||
# AccessDenied with bucket info
|
||||
if "AccessDenied" in response_body and "s3.amazonaws.com" in response_body:
|
||||
return True, 0.5, "S3 bucket exists but access denied - bucket enumerated"
|
||||
|
||||
# Public write/upload success
|
||||
if response_status in [200, 204] and context.get("is_upload_test"):
|
||||
headers_lower = {k.lower(): v for k, v in response_headers.items()}
|
||||
if "x-amz-request-id" in headers_lower:
|
||||
return True, 0.9, "S3 bucket allows public write access"
|
||||
|
||||
# Bucket policy exposed
|
||||
if '"Statement"' in response_body and '"Effect"' in response_body:
|
||||
if '"s3:' in response_body:
|
||||
return True, 0.85, "S3 bucket policy exposed"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class CloudMetadataExposureTester(BaseTester):
|
||||
"""Tester for Cloud Metadata Exposure vulnerabilities (SSRF to metadata)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "cloud_metadata_exposure"
|
||||
self.metadata_indicators = {
|
||||
# AWS
|
||||
"aws": [
|
||||
(r"ami-[0-9a-f]{8,17}", "AWS AMI ID"),
|
||||
(r"i-[0-9a-f]{8,17}", "AWS Instance ID"),
|
||||
(r"arn:aws:[a-z0-9-]+:[a-z0-9-]*:\d{12}:", "AWS ARN"),
|
||||
(r"AKIA[0-9A-Z]{16}", "AWS Access Key"),
|
||||
(r"169\.254\.169\.254", "AWS metadata endpoint"),
|
||||
(r"\"(?:AccessKeyId|SecretAccessKey|Token)\"", "AWS IAM credentials"),
|
||||
(r"ec2\.internal", "AWS internal hostname"),
|
||||
(r"\"accountId\"\s*:\s*\"\d{12}\"", "AWS Account ID"),
|
||||
],
|
||||
# GCP
|
||||
"gcp": [
|
||||
(r"projects/\d+/", "GCP Project reference"),
|
||||
(r"metadata\.google\.internal", "GCP metadata endpoint"),
|
||||
(r"\"access_token\"\s*:\s*\"ya29\.", "GCP OAuth token"),
|
||||
(r"compute\.googleapis\.com", "GCP Compute API"),
|
||||
(r"serviceAccounts/[^/]+/token", "GCP service account token"),
|
||||
],
|
||||
# Azure
|
||||
"azure": [
|
||||
(r"metadata\.azure\.com", "Azure metadata endpoint"),
|
||||
(r"(?:subscriptionId|resourceGroupName)\"\s*:\s*\"", "Azure resource info"),
|
||||
(r"\.blob\.core\.windows\.net", "Azure Blob Storage"),
|
||||
(r"\.vault\.azure\.net", "Azure Key Vault"),
|
||||
(r"\"access_token\"\s*:\s*\"eyJ", "Azure JWT token"),
|
||||
],
|
||||
}
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for cloud metadata in response"""
|
||||
findings = []
|
||||
|
||||
for provider, patterns in self.metadata_indicators.items():
|
||||
for pattern, description in patterns:
|
||||
if re.search(pattern, response_body):
|
||||
findings.append(f"{provider.upper()}: {description}")
|
||||
|
||||
if findings:
|
||||
# IAM credentials or tokens are critical
|
||||
critical = any(k in f for f in findings for k in ["credentials", "Access Key", "token", "Token"])
|
||||
confidence = 0.95 if critical else 0.8
|
||||
return True, confidence, f"Cloud metadata exposure: {', '.join(findings[:3])}"
|
||||
|
||||
# Check for metadata endpoint access (SSRF to cloud metadata)
|
||||
metadata_urls = ["169.254.169.254", "metadata.google.internal",
|
||||
"metadata.azure.com", "100.100.100.200"]
|
||||
for url in metadata_urls:
|
||||
if url in payload and response_status == 200 and len(response_body) > 50:
|
||||
return True, 0.85, f"Cloud metadata accessible via SSRF ({url})"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class SubdomainTakeoverTester(BaseTester):
|
||||
"""Tester for Subdomain Takeover vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "subdomain_takeover"
|
||||
self.takeover_fingerprints = [
|
||||
# AWS S3
|
||||
(r"NoSuchBucket", "S3 bucket - NoSuchBucket"),
|
||||
(r"The specified bucket does not exist", "S3 bucket does not exist"),
|
||||
# GitHub Pages
|
||||
(r"There isn't a GitHub Pages site here", "GitHub Pages - unclaimed"),
|
||||
(r"For root URLs.*GitHub Pages", "GitHub Pages not configured"),
|
||||
# Heroku
|
||||
(r"No such app", "Heroku - app not found"),
|
||||
(r"herokucdn\.com/error-pages/no-such-app", "Heroku - no such app"),
|
||||
# Shopify
|
||||
(r"Sorry, this shop is currently unavailable", "Shopify - shop unavailable"),
|
||||
# Tumblr
|
||||
(r"There's nothing here\.", "Tumblr - unclaimed"),
|
||||
(r"Whatever you were looking for doesn't currently exist", "Tumblr not found"),
|
||||
# WordPress.com
|
||||
(r"Do you want to register.*wordpress\.com", "WordPress.com - unclaimed"),
|
||||
# Azure
|
||||
(r"404 Web Site not found", "Azure - web app not found"),
|
||||
# Fastly
|
||||
(r"Fastly error: unknown domain", "Fastly - unknown domain"),
|
||||
# Pantheon
|
||||
(r"404 error unknown site", "Pantheon - unknown site"),
|
||||
# Zendesk
|
||||
(r"Help Center Closed", "Zendesk - closed"),
|
||||
# Unbounce
|
||||
(r"The requested URL was not found on this server.*unbounce", "Unbounce - not found"),
|
||||
# Surge.sh
|
||||
(r"project not found", "Surge.sh - not found"),
|
||||
# Fly.io
|
||||
(r"404.*fly\.io", "Fly.io - not found"),
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for cloud provider error pages indicating takeover opportunity"""
|
||||
for pattern, description in self.takeover_fingerprints:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.9, f"Subdomain takeover: {description}"
|
||||
|
||||
# CNAME pointing to unclaimed resource
|
||||
if context.get("cname_target"):
|
||||
cname = context["cname_target"]
|
||||
unclaimed_domains = [
|
||||
".s3.amazonaws.com", ".herokuapp.com", ".github.io",
|
||||
".azurewebsites.net", ".cloudfront.net", ".fastly.net",
|
||||
".ghost.io", ".myshopify.com", ".surge.sh",
|
||||
]
|
||||
for domain in unclaimed_domains:
|
||||
if cname.endswith(domain) and response_status in [404, 0]:
|
||||
return True, 0.85, f"Subdomain takeover: CNAME to {cname} returns {response_status}"
|
||||
|
||||
# NXDOMAIN with CNAME
|
||||
if context.get("dns_nxdomain") and context.get("has_cname"):
|
||||
return True, 0.8, "Subdomain takeover: CNAME exists but target domain is NXDOMAIN"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class VulnerableDependencyTester(BaseTester):
|
||||
"""Tester for Vulnerable Dependency detection"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "vulnerable_dependency"
|
||||
self.vulnerable_libs = [
|
||||
# JavaScript
|
||||
(r"jquery[/.-](?:1\.\d+|2\.\d+|3\.[0-4]\.\d+)", "jQuery < 3.5.0 (XSS)"),
|
||||
(r"angular[/.-]1\.[0-5]\.\d+", "AngularJS < 1.6 (sandbox escape)"),
|
||||
(r"lodash[/.-](?:[0-3]\.\d+|4\.(?:1[0-6]|[0-9])\.\d+)", "Lodash < 4.17.21 (prototype pollution)"),
|
||||
(r"bootstrap[/.-](?:[1-3]\.\d+|4\.[0-3]\.\d+)", "Bootstrap < 4.3.1 (XSS)"),
|
||||
(r"moment[/.-](?:[01]\.\d+|2\.(?:[0-9]|1[0-8])\.\d+)", "Moment.js < 2.19.3 (ReDoS)"),
|
||||
(r"handlebars[/.-](?:[0-3]\.\d+|4\.[0-6]\.\d+)", "Handlebars < 4.7.7 (prototype pollution)"),
|
||||
# Python
|
||||
(r"Django[/=](?:1\.\d+|2\.[01]\.\d+|3\.0\.\d+)", "Django < 3.1 (multiple CVEs)"),
|
||||
(r"Flask[/=](?:0\.\d+|1\.[01]\.\d+)", "Flask < 2.0 (known issues)"),
|
||||
(r"requests[/=]2\.(?:[0-9]|1\d|2[0-4])\.\d+", "Requests < 2.25 (CVE-2023-32681)"),
|
||||
# Java
|
||||
(r"log4j[/-]2\.(?:[0-9]|1[0-4])\.\d+", "Log4j < 2.15 (Log4Shell CVE-2021-44228)"),
|
||||
(r"spring-core[/-](?:[1-4]\.\d+|5\.[0-2]\.\d+)", "Spring < 5.3 (Spring4Shell)"),
|
||||
(r"jackson-databind[/-]2\.(?:[0-8]|9\.[0-9])\.\d*", "Jackson < 2.9.10 (deserialization)"),
|
||||
# PHP
|
||||
(r"laravel/framework[/:]\s*v?(?:[1-7]\.\d+|8\.[0-7]\d)", "Laravel < 8.80 (known issues)"),
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for known vulnerable library version strings"""
|
||||
findings = []
|
||||
|
||||
# Check response body (JS files, package.json, error pages)
|
||||
for pattern, description in self.vulnerable_libs:
|
||||
match = re.search(pattern, response_body, re.IGNORECASE)
|
||||
if match:
|
||||
findings.append(f"{match.group(0)} - {description}")
|
||||
|
||||
# Check headers for version info
|
||||
headers_str = "\n".join(f"{k}: {v}" for k, v in response_headers.items())
|
||||
for pattern, description in self.vulnerable_libs:
|
||||
match = re.search(pattern, headers_str, re.IGNORECASE)
|
||||
if match and description not in str(findings):
|
||||
findings.append(f"{match.group(0)} - {description}")
|
||||
|
||||
if findings:
|
||||
# Log4Shell and Spring4Shell are critical
|
||||
critical = any(k in f for f in findings for k in ["Log4Shell", "Spring4Shell", "deserialization"])
|
||||
confidence = 0.9 if critical else 0.75
|
||||
return True, confidence, f"Vulnerable dependency: {'; '.join(findings[:3])}"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ContainerEscapeTester(BaseTester):
|
||||
"""Tester for Container Escape / Container Misconfiguration vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "container_escape"
|
||||
self.container_indicators = [
|
||||
# Docker
|
||||
(r"\.dockerenv", "Docker environment file accessible"),
|
||||
(r"docker\.sock", "Docker socket exposed"),
|
||||
(r"/var/run/docker\.sock", "Docker socket path"),
|
||||
# Cgroup
|
||||
(r"docker[/-][0-9a-f]{12,64}", "Docker container cgroup"),
|
||||
(r"/proc/self/cgroup.*docker", "Docker cgroup detected"),
|
||||
(r"/proc/self/cgroup.*kubepods", "Kubernetes pod cgroup"),
|
||||
# Kubernetes
|
||||
(r"KUBERNETES_SERVICE_HOST", "Kubernetes service host env"),
|
||||
(r"KUBERNETES_PORT", "Kubernetes port env"),
|
||||
(r"/var/run/secrets/kubernetes\.io", "Kubernetes secrets path"),
|
||||
(r"serviceaccount/token", "Kubernetes service account token"),
|
||||
(r"kube-system", "Kubernetes system namespace"),
|
||||
# Container runtime
|
||||
(r"containerd", "containerd runtime"),
|
||||
(r"runc", "runc runtime"),
|
||||
# Process namespace
|
||||
(r"process\s+1\b.*(?:init|systemd|tini|dumb-init)", "Container init process"),
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for Docker/container indicators in response"""
|
||||
findings = []
|
||||
|
||||
for pattern, description in self.container_indicators:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
findings.append(description)
|
||||
|
||||
if findings:
|
||||
# Docker socket or K8s secrets are critical
|
||||
critical = any(k in f for f in findings for k in ["socket", "secrets", "token"])
|
||||
confidence = 0.9 if critical else 0.7
|
||||
return True, confidence, f"Container exposure: {', '.join(findings[:3])}"
|
||||
|
||||
# Privileged container detection
|
||||
if context.get("is_capability_check"):
|
||||
# Check for extra capabilities
|
||||
cap_patterns = [
|
||||
r"cap_sys_admin", r"cap_sys_ptrace", r"cap_net_admin",
|
||||
r"cap_dac_override", r"cap_sys_rawio",
|
||||
]
|
||||
for pattern in cap_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.8, f"Privileged container: {pattern} capability detected"
|
||||
|
||||
# Mount namespace check
|
||||
if re.search(r"/dev/(?:sda|xvda|nvme)\d*\s", response_body):
|
||||
return True, 0.6, "Host block devices visible from container"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ServerlessMisconfigTester(BaseTester):
|
||||
"""Tester for Serverless Misconfiguration vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "serverless_misconfig"
|
||||
self.env_patterns = [
|
||||
# AWS Lambda
|
||||
(r"AWS_LAMBDA_FUNCTION_NAME\s*[=:]\s*(\S+)", "Lambda function name"),
|
||||
(r"AWS_SECRET_ACCESS_KEY\s*[=:]\s*(\S+)", "Lambda AWS secret key"),
|
||||
(r"AWS_SESSION_TOKEN\s*[=:]\s*(\S+)", "Lambda session token"),
|
||||
(r"AWS_LAMBDA_LOG_GROUP_NAME", "Lambda log group"),
|
||||
(r"_HANDLER\s*[=:]\s*(\S+)", "Lambda handler path"),
|
||||
(r"LAMBDA_TASK_ROOT\s*[=:]\s*(\S+)", "Lambda task root"),
|
||||
(r"AWS_EXECUTION_ENV\s*[=:]\s*(\S+)", "Lambda execution environment"),
|
||||
# Google Cloud Functions
|
||||
(r"FUNCTION_NAME\s*[=:]\s*(\S+)", "Cloud Function name"),
|
||||
(r"GCLOUD_PROJECT\s*[=:]\s*(\S+)", "GCP project ID"),
|
||||
(r"GOOGLE_CLOUD_PROJECT\s*[=:]\s*(\S+)", "GCP project"),
|
||||
(r"GCP_PROJECT\s*[=:]\s*(\S+)", "GCP project"),
|
||||
(r"FUNCTION_REGION\s*[=:]\s*(\S+)", "Cloud Function region"),
|
||||
# Azure Functions
|
||||
(r"FUNCTIONS_WORKER_RUNTIME\s*[=:]\s*(\S+)", "Azure Function runtime"),
|
||||
(r"AzureWebJobsStorage\s*[=:]\s*(\S+)", "Azure storage connection string"),
|
||||
(r"WEBSITE_SITE_NAME\s*[=:]\s*(\S+)", "Azure site name"),
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for serverless environment variable exposure"""
|
||||
findings = []
|
||||
|
||||
for pattern, description in self.env_patterns:
|
||||
match = re.search(pattern, response_body)
|
||||
if match:
|
||||
value = match.group(1) if match.lastindex else ""
|
||||
# Redact sensitive values
|
||||
if "key" in description.lower() or "token" in description.lower() or "secret" in description.lower():
|
||||
display = f"{description} (REDACTED)"
|
||||
else:
|
||||
display = f"{description}: {value[:30]}"
|
||||
findings.append(display)
|
||||
|
||||
if findings:
|
||||
# Credentials are critical
|
||||
critical = any(k in f for f in findings for k in ["secret", "token", "REDACTED"])
|
||||
confidence = 0.95 if critical else 0.75
|
||||
return True, confidence, f"Serverless misconfiguration: {', '.join(findings[:3])}"
|
||||
|
||||
# Function source code exposure
|
||||
if context.get("is_source_request"):
|
||||
source_indicators = [
|
||||
r"exports\.handler\s*=", r"def lambda_handler\(",
|
||||
r"def main\(req:", r"module\.exports",
|
||||
r"def hello_http\(request\):",
|
||||
]
|
||||
for pattern in source_indicators:
|
||||
if re.search(pattern, response_body):
|
||||
return True, 0.8, "Serverless misconfiguration: function source code exposed"
|
||||
|
||||
# Invocation error details
|
||||
error_patterns = [
|
||||
r"\"errorType\"\s*:\s*\"(\w+)\"",
|
||||
r"\"stackTrace\"\s*:\s*\[",
|
||||
r"Runtime\.HandlerNotFound",
|
||||
r"\"errorMessage\"\s*:\s*\".*(?:import|require|module)",
|
||||
]
|
||||
for pattern in error_patterns:
|
||||
match = re.search(pattern, response_body)
|
||||
if match:
|
||||
return True, 0.7, f"Serverless misconfiguration: detailed error exposed ({match.group(0)[:60]})"
|
||||
|
||||
return False, 0.0, None
|
||||
@@ -0,0 +1,388 @@
|
||||
"""
|
||||
NeuroSploit v3 - Data Exposure Vulnerability Testers
|
||||
|
||||
Testers for sensitive data exposure, information disclosure, API key exposure,
|
||||
source code disclosure, backup file exposure, and version disclosure.
|
||||
"""
|
||||
import re
|
||||
from typing import Tuple, Dict, Optional
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
|
||||
class SensitiveDataExposureTester(BaseTester):
|
||||
"""Tester for Sensitive Data Exposure (PII leakage)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "sensitive_data_exposure"
|
||||
self.pii_patterns = [
|
||||
# SSN (US)
|
||||
(r"\b\d{3}-\d{2}-\d{4}\b", "SSN pattern"),
|
||||
# Credit card numbers (Visa, MC, Amex, Discover)
|
||||
(r"\b4\d{3}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", "Visa card number"),
|
||||
(r"\b5[1-5]\d{2}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", "MasterCard number"),
|
||||
(r"\b3[47]\d{2}[\s-]?\d{6}[\s-]?\d{5}\b", "Amex card number"),
|
||||
(r"\b6(?:011|5\d{2})[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", "Discover card number"),
|
||||
# Email addresses in bulk (10+ suggests a data leak)
|
||||
(r"[\w.+-]+@[\w-]+\.[\w.-]+", "email address"),
|
||||
# Phone numbers (US format)
|
||||
(r"\b\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}\b", "phone number"),
|
||||
# Passport numbers
|
||||
(r"\b[A-Z]\d{8}\b", "passport number pattern"),
|
||||
# Private keys
|
||||
(r"-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----", "private key"),
|
||||
# Password hashes
|
||||
(r"\$2[aby]?\$\d{1,2}\$[./A-Za-z0-9]{53}", "bcrypt hash"),
|
||||
(r"\b[a-f0-9]{32}\b", "MD5 hash"),
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for PII patterns in response"""
|
||||
if response_status >= 400:
|
||||
return False, 0.0, None
|
||||
|
||||
findings = []
|
||||
|
||||
for pattern, description in self.pii_patterns:
|
||||
matches = re.findall(pattern, response_body)
|
||||
if matches:
|
||||
# Private keys and password hashes are always significant
|
||||
if "private key" in description:
|
||||
return True, 0.95, f"Sensitive data exposure: {description} found in response"
|
||||
if "bcrypt hash" in description:
|
||||
return True, 0.9, f"Sensitive data exposure: {description} found in response"
|
||||
|
||||
# For patterns like emails, phone numbers - check for bulk exposure
|
||||
if description in ["email address", "phone number"]:
|
||||
if len(matches) >= 5:
|
||||
findings.append(f"{len(matches)} {description}s")
|
||||
elif description == "MD5 hash":
|
||||
if len(matches) >= 3:
|
||||
findings.append(f"{len(matches)} {description}es")
|
||||
else:
|
||||
findings.append(f"{description} ({matches[0][:20]}...)")
|
||||
|
||||
if findings:
|
||||
confidence = min(0.9, 0.6 + 0.1 * len(findings))
|
||||
return True, confidence, f"Sensitive data exposure: {', '.join(findings[:3])}"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class InformationDisclosureTester(BaseTester):
|
||||
"""Tester for Information Disclosure vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "information_disclosure"
|
||||
self.disclosure_patterns = [
|
||||
# Server version headers
|
||||
(r"Server:\s*(.+)", "header", "Server version"),
|
||||
(r"X-Powered-By:\s*(.+)", "header", "Technology stack"),
|
||||
(r"X-AspNet-Version:\s*(.+)", "header", "ASP.NET version"),
|
||||
(r"X-AspNetMvc-Version:\s*(.+)", "header", "ASP.NET MVC version"),
|
||||
]
|
||||
self.body_patterns = [
|
||||
# Path disclosure
|
||||
(r"(?:/var/www|/home/\w+|/srv/|/opt/\w+|C:\\inetpub|C:\\Users\\\w+)[/\\]\S+", "Internal path"),
|
||||
# Stack traces
|
||||
(r"Traceback \(most recent call last\)", "Python stack trace"),
|
||||
(r"at \w+\.\w+\([\w.]+:\d+\)", "Java stack trace"),
|
||||
(r"(?:Fatal error|Warning|Notice):\s+.*\sin\s+/\S+\s+on line \d+", "PHP error with path"),
|
||||
(r"Microsoft \.NET Framework Version:\d+", ".NET framework version"),
|
||||
# Database info
|
||||
(r"(?:MySQL|PostgreSQL|Oracle|MSSQL)\s+\d+\.\d+", "Database version"),
|
||||
# Debug info
|
||||
(r"(?:DEBUG|TRACE)\s*=\s*(?:true|True|1)", "Debug mode enabled"),
|
||||
(r"(?:SECRET_KEY|DB_PASSWORD|API_SECRET)\s*[=:]\s*\S+", "Secret in debug output"),
|
||||
# Internal IPs
|
||||
(r"\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b", "Internal IP address"),
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for information disclosure in headers and body"""
|
||||
findings = []
|
||||
|
||||
# Check headers
|
||||
headers_str = "\n".join(f"{k}: {v}" for k, v in response_headers.items())
|
||||
for pattern, location, description in self.disclosure_patterns:
|
||||
match = re.search(pattern, headers_str, re.IGNORECASE)
|
||||
if match:
|
||||
findings.append(f"{description}: {match.group(1)[:50]}")
|
||||
|
||||
# Check body
|
||||
for pattern, description in self.body_patterns:
|
||||
match = re.search(pattern, response_body, re.IGNORECASE)
|
||||
if match:
|
||||
findings.append(f"{description}: {match.group(0)[:80]}")
|
||||
|
||||
if findings:
|
||||
# Stack traces and secrets are higher severity
|
||||
high_severity = any("stack trace" in f.lower() or "secret" in f.lower() or "debug" in f.lower() for f in findings)
|
||||
confidence = 0.85 if high_severity else 0.7
|
||||
return True, confidence, f"Information disclosure: {'; '.join(findings[:3])}"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ApiKeyExposureTester(BaseTester):
|
||||
"""Tester for API Key Exposure vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "api_key_exposure"
|
||||
self.key_patterns = [
|
||||
# AWS
|
||||
(r"AKIA[0-9A-Z]{16}", "AWS Access Key"),
|
||||
(r"(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*[A-Za-z0-9/+=]{40}", "AWS Secret Key"),
|
||||
# Google
|
||||
(r"AIza[0-9A-Za-z\-_]{35}", "Google API Key"),
|
||||
(r"ya29\.[0-9A-Za-z\-_]+", "Google OAuth Token"),
|
||||
# Stripe
|
||||
(r"sk_live_[0-9a-zA-Z]{24,}", "Stripe Secret Key"),
|
||||
(r"pk_live_[0-9a-zA-Z]{24,}", "Stripe Publishable Key"),
|
||||
(r"rk_live_[0-9a-zA-Z]{24,}", "Stripe Restricted Key"),
|
||||
# GitHub
|
||||
(r"gh[pousr]_[A-Za-z0-9_]{36,}", "GitHub Token"),
|
||||
# Slack
|
||||
(r"xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*", "Slack Token"),
|
||||
# Twilio
|
||||
(r"SK[0-9a-fA-F]{32}", "Twilio API Key"),
|
||||
# SendGrid
|
||||
(r"SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}", "SendGrid API Key"),
|
||||
# Heroku
|
||||
(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", "Heroku API Key / UUID"),
|
||||
# Generic patterns
|
||||
(r"(?:api[_-]?key|apikey|api[_-]?secret)\s*[=:]\s*['\"]?([A-Za-z0-9_\-]{20,})['\"]?", "Generic API Key"),
|
||||
(r"(?:access[_-]?token|auth[_-]?token)\s*[=:]\s*['\"]?([A-Za-z0-9_\-.]{20,})['\"]?", "Access Token"),
|
||||
# Bearer tokens in JS
|
||||
(r"['\"]Bearer\s+[A-Za-z0-9_\-\.]{20,}['\"]", "Hardcoded Bearer Token"),
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for API key patterns in response"""
|
||||
findings = []
|
||||
|
||||
for pattern, description in self.key_patterns:
|
||||
matches = re.findall(pattern, response_body)
|
||||
if matches:
|
||||
# Skip UUIDs unless in specific context (too many false positives)
|
||||
if "UUID" in description:
|
||||
continue
|
||||
# Redact the actual key in evidence
|
||||
sample = matches[0] if isinstance(matches[0], str) else matches[0]
|
||||
redacted = sample[:8] + "..." + sample[-4:] if len(sample) > 12 else sample[:4] + "..."
|
||||
findings.append(f"{description} ({redacted})")
|
||||
|
||||
if findings:
|
||||
# AWS/Stripe secret keys are critical
|
||||
critical = any(k in f for f in findings for k in ["AWS Secret", "Stripe Secret", "Secret Key"])
|
||||
confidence = 0.95 if critical else 0.85
|
||||
return True, confidence, f"API key exposure: {', '.join(findings[:3])}"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class SourceCodeDisclosureTester(BaseTester):
|
||||
"""Tester for Source Code Disclosure vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "source_code_disclosure"
|
||||
self.source_indicators = [
|
||||
# Git
|
||||
(r"\[core\]\s*\n\s*repositoryformatversion", "Git config exposed"),
|
||||
(r"\[remote \"origin\"\]", "Git config with remote"),
|
||||
(r"ref: refs/heads/", "Git HEAD reference exposed"),
|
||||
# Source maps
|
||||
(r"\"version\"\s*:\s*3,\s*\"sources\"", "JavaScript source map"),
|
||||
(r"//[#@]\s*sourceMappingURL=", "Source map reference"),
|
||||
# PHP source
|
||||
(r"<\?php\s", "PHP source code"),
|
||||
(r"<\?=", "PHP short tag source"),
|
||||
# Python source
|
||||
(r"^(?:import |from \w+ import |def \w+\(|class \w+)", "Python source code"),
|
||||
# Java/JSP
|
||||
(r"<%@?\s*page\s+", "JSP source code"),
|
||||
(r"package\s+\w+\.\w+;", "Java package declaration"),
|
||||
# Environment files
|
||||
(r"(?:DB_PASSWORD|SECRET_KEY|DATABASE_URL)\s*=\s*\S+", "Environment file content"),
|
||||
# Composer/package files with private repos
|
||||
(r"\"require\".*\"private/", "Private package reference"),
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for source code indicators in response"""
|
||||
if response_status >= 400:
|
||||
return False, 0.0, None
|
||||
|
||||
for pattern, description in self.source_indicators:
|
||||
match = re.search(pattern, response_body, re.MULTILINE)
|
||||
if match:
|
||||
# Git config is high confidence
|
||||
if "Git" in description:
|
||||
return True, 0.95, f"Source code disclosure: {description}"
|
||||
# Environment file is critical
|
||||
if "Environment" in description:
|
||||
return True, 0.95, f"Source code disclosure: {description}"
|
||||
# Source maps lower confidence (often intentional)
|
||||
if "source map" in description.lower():
|
||||
return True, 0.6, f"Source code disclosure: {description}"
|
||||
return True, 0.8, f"Source code disclosure: {description}"
|
||||
|
||||
# Check for common source file access patterns
|
||||
source_paths = [".git/config", ".env", ".htaccess", "web.config",
|
||||
"wp-config.php", "config.php", "settings.py"]
|
||||
for path in source_paths:
|
||||
if path in payload and response_status == 200 and len(response_body) > 50:
|
||||
return True, 0.7, f"Source code disclosure: {path} accessible"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class BackupFileExposureTester(BaseTester):
|
||||
"""Tester for Backup File Exposure vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "backup_file_exposure"
|
||||
self.file_signatures = [
|
||||
# SQL dumps
|
||||
(r"-- MySQL dump \d+", "MySQL database dump"),
|
||||
(r"-- PostgreSQL database dump", "PostgreSQL database dump"),
|
||||
(r"CREATE TABLE\s+[`\"]\w+[`\"]", "SQL DDL statements"),
|
||||
(r"INSERT INTO\s+[`\"]\w+[`\"]", "SQL data dump"),
|
||||
# Archive signatures (in text responses)
|
||||
(r"PK\x03\x04", "ZIP archive"),
|
||||
# Tar
|
||||
(r"ustar\s", "TAR archive"),
|
||||
# Config backups
|
||||
(r"<\?xml.*<configuration>", "XML configuration backup"),
|
||||
(r"server\s*\{[^}]*listen\s+\d+", "Nginx config backup"),
|
||||
(r"<VirtualHost\s+", "Apache config backup"),
|
||||
]
|
||||
self.backup_extensions = [
|
||||
".bak", ".backup", ".old", ".orig", ".save",
|
||||
".swp", ".swo", ".tmp", ".temp", ".copy",
|
||||
"~", ".sql", ".tar.gz", ".zip", ".dump",
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for backup file content in response"""
|
||||
if response_status >= 400:
|
||||
return False, 0.0, None
|
||||
|
||||
# Check file signatures
|
||||
for pattern, description in self.file_signatures:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.9, f"Backup file exposure: {description} detected"
|
||||
|
||||
# Check if backup extension in request returned content
|
||||
for ext in self.backup_extensions:
|
||||
if ext in payload and response_status == 200 and len(response_body) > 100:
|
||||
headers_lower = {k.lower(): v for k, v in response_headers.items()}
|
||||
content_type = headers_lower.get("content-type", "").lower()
|
||||
# Non-HTML responses to backup file requests are suspicious
|
||||
if "text/html" not in content_type:
|
||||
return True, 0.75, f"Backup file exposure: {ext} file served ({content_type})"
|
||||
# HTML response but contains code-like content
|
||||
if re.search(r"(?:function |class |import |require\(|define\()", response_body):
|
||||
return True, 0.7, f"Backup file exposure: {ext} file contains source code"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class VersionDisclosureTester(BaseTester):
|
||||
"""Tester for Version Disclosure mapping to known CVEs"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "version_disclosure"
|
||||
# Software versions with known critical CVEs
|
||||
self.vulnerable_versions = {
|
||||
r"Apache/2\.4\.49\b": "CVE-2021-41773 (path traversal)",
|
||||
r"Apache/2\.4\.50\b": "CVE-2021-42013 (path traversal bypass)",
|
||||
r"nginx/1\.(?:[0-9]|1[0-7])\.\d+": "Potential nginx < 1.18 vulnerabilities",
|
||||
r"PHP/(?:5\.\d|7\.[0-3])\.\d+": "Outdated PHP version with known CVEs",
|
||||
r"OpenSSL/1\.0\.\d": "OpenSSL 1.0.x - multiple known CVEs",
|
||||
r"jQuery/(?:1\.\d|2\.\d|3\.[0-4])\.\d+": "jQuery < 3.5 - XSS via htmlPrefilter",
|
||||
r"WordPress/(?:[1-4]\.\d|5\.[0-7])": "Outdated WordPress version",
|
||||
r"Drupal/(?:[1-7]\.\d|8\.[0-5])": "Outdated Drupal version",
|
||||
r"Rails/(?:[1-4]\.\d|5\.[01])": "Outdated Rails version",
|
||||
r"Spring Framework/(?:[1-4]\.\d|5\.[0-2])": "Outdated Spring version",
|
||||
r"Express/(?:[1-3]\.\d|4\.(?:1[0-6]))": "Outdated Express.js version",
|
||||
r"Django/(?:1\.\d|2\.[01]|3\.0)": "Outdated Django version",
|
||||
r"Log4j.(?:2\.(?:0|1[0-4])\.\d)": "CVE-2021-44228 (Log4Shell)",
|
||||
r"Tomcat/(?:[1-8]\.\d|9\.[0-3]\d\.\d)": "Potentially outdated Tomcat",
|
||||
r"IIS/(?:[1-9]\.0|10\.0)": "IIS version disclosure",
|
||||
}
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for version strings mapping to known CVEs"""
|
||||
# Combine headers and body for scanning
|
||||
headers_str = "\n".join(f"{k}: {v}" for k, v in response_headers.items())
|
||||
full_text = headers_str + "\n" + response_body
|
||||
|
||||
findings = []
|
||||
|
||||
for pattern, cve_info in self.vulnerable_versions.items():
|
||||
match = re.search(pattern, full_text, re.IGNORECASE)
|
||||
if match:
|
||||
version_str = match.group(0)
|
||||
findings.append(f"{version_str} - {cve_info}")
|
||||
|
||||
if findings:
|
||||
# Known CVEs are high confidence
|
||||
has_cve = any("CVE-" in f for f in findings)
|
||||
confidence = 0.9 if has_cve else 0.7
|
||||
return True, confidence, f"Version disclosure: {'; '.join(findings[:3])}"
|
||||
|
||||
# Generic version disclosure in Server header
|
||||
headers_lower = {k.lower(): v for k, v in response_headers.items()}
|
||||
server = headers_lower.get("server", "")
|
||||
if re.search(r"/\d+\.\d+", server):
|
||||
return True, 0.5, f"Version disclosure in Server header: {server}"
|
||||
|
||||
return False, 0.0, None
|
||||
@@ -0,0 +1,356 @@
|
||||
"""
|
||||
NeuroSploit v3 - File Access Vulnerability Testers
|
||||
|
||||
Testers for LFI, RFI, Path Traversal, XXE, File Upload
|
||||
"""
|
||||
import re
|
||||
from typing import Tuple, Dict, Optional
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
|
||||
class LFITester(BaseTester):
|
||||
"""Tester for Local File Inclusion"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "lfi"
|
||||
self.file_signatures = {
|
||||
# Linux files
|
||||
r"root:.*:0:0:": "/etc/passwd",
|
||||
r"\[boot loader\]": "Windows boot.ini",
|
||||
r"\[operating systems\]": "Windows boot.ini",
|
||||
r"# /etc/hosts": "/etc/hosts",
|
||||
r"localhost": "/etc/hosts",
|
||||
r"\[global\]": "Samba config",
|
||||
r"include.*php": "PHP config",
|
||||
# Windows files
|
||||
r"\[extensions\]": "Windows win.ini",
|
||||
r"for 16-bit app support": "Windows system.ini",
|
||||
}
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for LFI indicators"""
|
||||
for pattern, file_name in self.file_signatures.items():
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.95, f"LFI confirmed: {file_name} content detected"
|
||||
|
||||
# Check for path in error messages
|
||||
path_patterns = [
|
||||
r"failed to open stream.*No such file",
|
||||
r"include\(.*\): failed to open stream",
|
||||
r"Warning.*file_get_contents",
|
||||
r"fopen\(.*\): failed"
|
||||
]
|
||||
for pattern in path_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.6, "LFI indicator: File operation error with path"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class RFITester(BaseTester):
|
||||
"""Tester for Remote File Inclusion"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "rfi"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for RFI indicators"""
|
||||
# Check if our remote content was included
|
||||
if "neurosploit_rfi_test" in response_body:
|
||||
return True, 0.95, "RFI confirmed: Remote content executed"
|
||||
|
||||
# Check for URL-related errors
|
||||
rfi_errors = [
|
||||
r"failed to open stream: HTTP request failed",
|
||||
r"allow_url_include",
|
||||
r"URL file-access is disabled"
|
||||
]
|
||||
for pattern in rfi_errors:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.5, f"RFI indicator: {pattern}"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class PathTraversalTester(BaseTester):
|
||||
"""Tester for Path Traversal"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "path_traversal"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for path traversal indicators"""
|
||||
# Same as LFI essentially
|
||||
file_contents = [
|
||||
r"root:.*:0:0:",
|
||||
r"\[boot loader\]",
|
||||
r"# /etc/",
|
||||
r"127\.0\.0\.1.*localhost"
|
||||
]
|
||||
for pattern in file_contents:
|
||||
if re.search(pattern, response_body):
|
||||
return True, 0.9, f"Path traversal successful: File content detected"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class XXETester(BaseTester):
|
||||
"""Tester for XML External Entity Injection"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "xxe"
|
||||
|
||||
def build_request(self, endpoint, payload: str) -> Tuple[str, Dict, Dict, Optional[str]]:
|
||||
"""Build XXE request with XML body"""
|
||||
headers = {
|
||||
"User-Agent": "NeuroSploit/3.0",
|
||||
"Content-Type": "application/xml"
|
||||
}
|
||||
return endpoint.url, {}, headers, payload
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for XXE indicators"""
|
||||
# File content indicators
|
||||
xxe_indicators = [
|
||||
r"root:.*:0:0:",
|
||||
r"\[boot loader\]",
|
||||
r"# /etc/hosts",
|
||||
r"<!ENTITY",
|
||||
]
|
||||
for pattern in xxe_indicators:
|
||||
if re.search(pattern, response_body):
|
||||
return True, 0.9, f"XXE confirmed: External entity processed"
|
||||
|
||||
# Error indicators
|
||||
xxe_errors = [
|
||||
r"XML parsing error",
|
||||
r"External entity",
|
||||
r"DOCTYPE.*ENTITY",
|
||||
r"libxml"
|
||||
]
|
||||
for pattern in xxe_errors:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.6, f"XXE indicator: XML error with entity reference"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class FileUploadTester(BaseTester):
|
||||
"""Tester for Arbitrary File Upload"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "file_upload"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for file upload vulnerability indicators"""
|
||||
# Check for successful upload indicators
|
||||
if response_status in [200, 201]:
|
||||
success_indicators = [
|
||||
"uploaded successfully",
|
||||
"file saved",
|
||||
"upload complete",
|
||||
'"success"\\s*:\\s*true',
|
||||
'"status"\\s*:\\s*"ok"'
|
||||
]
|
||||
for pattern in success_indicators:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.7, "File uploaded successfully - verify execution"
|
||||
|
||||
# Check for path disclosure in response
|
||||
if re.search(r'["\']?(?:path|url|file)["\']?\s*:\s*["\'][^"\']+\.(php|asp|jsp)', response_body, re.IGNORECASE):
|
||||
return True, 0.8, "Executable file path returned - possible RCE"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ArbitraryFileReadTester(BaseTester):
|
||||
"""Tester for Arbitrary File Read vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "arbitrary_file_read"
|
||||
self.sensitive_file_patterns = {
|
||||
# /etc/passwd format
|
||||
r"root:.*:0:0:": "/etc/passwd",
|
||||
r"daemon:.*:\d+:\d+:": "/etc/passwd",
|
||||
r"nobody:.*:\d+:\d+:": "/etc/passwd",
|
||||
# .env file patterns
|
||||
r"(?:DB_PASSWORD|DATABASE_URL|SECRET_KEY|API_KEY|APP_SECRET)\s*=": ".env file",
|
||||
r"(?:AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY)\s*=": ".env file (AWS credentials)",
|
||||
# SSH key headers
|
||||
r"-----BEGIN (?:RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----": "SSH/TLS private key",
|
||||
r"-----BEGIN CERTIFICATE-----": "TLS certificate",
|
||||
# Shadow file
|
||||
r"root:\$[0-9a-z]+\$": "/etc/shadow",
|
||||
# Config files
|
||||
r"<\?php.*\$db": "PHP config with DB credentials",
|
||||
}
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for sensitive file contents in response"""
|
||||
for pattern, file_desc in self.sensitive_file_patterns.items():
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.95, f"Arbitrary file read confirmed: {file_desc} content detected"
|
||||
|
||||
# Check for base64-encoded sensitive content
|
||||
base64_pattern = re.findall(r'[A-Za-z0-9+/]{40,}={0,2}', response_body)
|
||||
for b64_match in base64_pattern[:5]: # Check first 5 matches
|
||||
try:
|
||||
import base64
|
||||
decoded = base64.b64decode(b64_match).decode('utf-8', errors='ignore')
|
||||
if re.search(r"root:.*:0:0:", decoded) or re.search(r"-----BEGIN.*PRIVATE KEY-----", decoded):
|
||||
return True, 0.9, "Arbitrary file read: Base64-encoded sensitive file content"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ArbitraryFileDeleteTester(BaseTester):
|
||||
"""Tester for Arbitrary File Delete vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "arbitrary_file_delete"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for successful file deletion indicators"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Check for explicit deletion success messages
|
||||
delete_success_patterns = [
|
||||
r"file\s+(?:has been\s+)?(?:deleted|removed)\s+successfully",
|
||||
r"(?:deleted|removed)\s+successfully",
|
||||
r'"success"\s*:\s*true.*(?:delet|remov)',
|
||||
r'"status"\s*:\s*"(?:deleted|removed)"',
|
||||
r'"message"\s*:\s*".*(?:deleted|removed).*"',
|
||||
]
|
||||
for pattern in delete_success_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.85, "Arbitrary file delete: Deletion success confirmed in response"
|
||||
|
||||
# Check for 200/204 on DELETE request with traversal path
|
||||
traversal_indicators = ["../", "..\\", "%2e%2e", "..%2f", "..%5c"]
|
||||
has_traversal = any(t in payload.lower() for t in traversal_indicators)
|
||||
|
||||
if has_traversal:
|
||||
if response_status == 204:
|
||||
return True, 0.8, "Arbitrary file delete: 204 No Content after path traversal delete"
|
||||
if response_status == 200:
|
||||
return True, 0.7, "Arbitrary file delete: 200 OK after path traversal delete request"
|
||||
|
||||
# Check for file-not-found on subsequent access (context-based)
|
||||
if context.get("follow_up_status") == 404:
|
||||
return True, 0.85, "Arbitrary file delete: File not found after deletion request"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ZipSlipTester(BaseTester):
|
||||
"""Tester for Zip Slip (path traversal in archive extraction)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "zip_slip"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for path traversal in archive extraction"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Check for traversal path acceptance in response
|
||||
traversal_patterns = [
|
||||
r"\.\./\.\./\.\./",
|
||||
r"\.\.\\\.\.\\\.\.\\",
|
||||
r"%2e%2e%2f",
|
||||
r"%2e%2e/",
|
||||
]
|
||||
for pattern in traversal_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
# Traversal path echoed in response
|
||||
if response_status in [200, 201]:
|
||||
return True, 0.8, "Zip Slip: Path traversal sequence accepted in archive extraction"
|
||||
|
||||
# Check for successful extraction with traversal payload
|
||||
if response_status in [200, 201]:
|
||||
extraction_success = [
|
||||
r"extract(?:ed|ion)\s+(?:successful|complete)",
|
||||
r"(?:file|archive)\s+(?:uploaded|processed)\s+successfully",
|
||||
r'"extracted"\s*:\s*true',
|
||||
r'"files"\s*:\s*\[.*\.\./.*\]',
|
||||
]
|
||||
for pattern in extraction_success:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
if any(t in payload for t in ["../", "..\\", "%2e%2e"]):
|
||||
return True, 0.85, "Zip Slip: Archive with traversal paths extracted successfully"
|
||||
|
||||
# Check for file written outside expected directory
|
||||
overwrite_indicators = [
|
||||
r"(?:overwr(?:ote|itten)|replaced)\s+.*(?:/etc/|/var/|/tmp/|C:\\)",
|
||||
r"(?:created|wrote)\s+.*\.\./",
|
||||
]
|
||||
for pattern in overwrite_indicators:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.9, "Zip Slip: File written outside extraction directory"
|
||||
|
||||
return False, 0.0, None
|
||||
@@ -0,0 +1,509 @@
|
||||
"""
|
||||
NeuroSploit v3 - Infrastructure Vulnerability Testers
|
||||
|
||||
Testers for Security Headers, SSL/TLS, HTTP Methods
|
||||
"""
|
||||
import re
|
||||
from typing import Tuple, Dict, Optional
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
|
||||
class SecurityHeadersTester(BaseTester):
|
||||
"""Tester for Missing Security Headers"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "security_headers"
|
||||
self.required_headers = {
|
||||
"Strict-Transport-Security": "HSTS not configured",
|
||||
"X-Content-Type-Options": "X-Content-Type-Options not set",
|
||||
"X-Frame-Options": "X-Frame-Options not set",
|
||||
"Content-Security-Policy": "CSP not configured",
|
||||
"X-XSS-Protection": "X-XSS-Protection not set (legacy but still useful)",
|
||||
"Referrer-Policy": "Referrer-Policy not configured"
|
||||
}
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for missing security headers"""
|
||||
missing = []
|
||||
headers_lower = {k.lower(): v for k, v in response_headers.items()}
|
||||
|
||||
for header, message in self.required_headers.items():
|
||||
if header.lower() not in headers_lower:
|
||||
missing.append(message)
|
||||
|
||||
# Check for weak CSP
|
||||
csp = headers_lower.get("content-security-policy", "")
|
||||
if csp:
|
||||
weak_csp = []
|
||||
if "unsafe-inline" in csp:
|
||||
weak_csp.append("unsafe-inline")
|
||||
if "unsafe-eval" in csp:
|
||||
weak_csp.append("unsafe-eval")
|
||||
if "*" in csp:
|
||||
weak_csp.append("wildcard sources")
|
||||
if weak_csp:
|
||||
missing.append(f"Weak CSP: {', '.join(weak_csp)}")
|
||||
|
||||
if missing:
|
||||
confidence = min(0.3 + len(missing) * 0.1, 0.8)
|
||||
return True, confidence, f"Missing/weak headers: {'; '.join(missing[:3])}"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class SSLTester(BaseTester):
|
||||
"""Tester for SSL/TLS Issues"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "ssl_issues"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for SSL/TLS issues"""
|
||||
issues = []
|
||||
|
||||
# Check HSTS
|
||||
hsts = response_headers.get("Strict-Transport-Security", "")
|
||||
if not hsts:
|
||||
issues.append("HSTS not enabled")
|
||||
else:
|
||||
# Check HSTS max-age
|
||||
max_age_match = re.search(r'max-age=(\d+)', hsts)
|
||||
if max_age_match:
|
||||
max_age = int(max_age_match.group(1))
|
||||
if max_age < 31536000: # Less than 1 year
|
||||
issues.append(f"HSTS max-age too short: {max_age}s")
|
||||
|
||||
if "includeSubDomains" not in hsts:
|
||||
issues.append("HSTS missing includeSubDomains")
|
||||
|
||||
# Check for HTTP resources on HTTPS page
|
||||
if "https://" in (context.get("url", "") or ""):
|
||||
http_resources = re.findall(r'(?:src|href)=["\']http://[^"\']+', response_body)
|
||||
if http_resources:
|
||||
issues.append(f"Mixed content: {len(http_resources)} HTTP resources")
|
||||
|
||||
if issues:
|
||||
return True, 0.6, f"SSL/TLS issues: {'; '.join(issues)}"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class HTTPMethodsTester(BaseTester):
|
||||
"""Tester for Dangerous HTTP Methods"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "http_methods"
|
||||
self.dangerous_methods = ["TRACE", "TRACK", "PUT", "DELETE", "CONNECT"]
|
||||
|
||||
def build_request(self, endpoint, payload: str) -> Tuple[str, Dict, Dict, Optional[str]]:
|
||||
"""Build OPTIONS request to check allowed methods"""
|
||||
headers = {
|
||||
"User-Agent": "NeuroSploit/3.0"
|
||||
}
|
||||
# payload is the HTTP method to test
|
||||
return endpoint.url, {}, headers, None
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for dangerous HTTP methods"""
|
||||
# Check Allow header from OPTIONS response
|
||||
allow = response_headers.get("Allow", "")
|
||||
dangerous_found = []
|
||||
|
||||
for method in self.dangerous_methods:
|
||||
if method in allow.upper():
|
||||
dangerous_found.append(method)
|
||||
|
||||
# TRACE method enables XST attacks
|
||||
if "TRACE" in dangerous_found or "TRACK" in dangerous_found:
|
||||
return True, 0.7, f"Dangerous methods enabled: {', '.join(dangerous_found)} (XST risk)"
|
||||
|
||||
if dangerous_found:
|
||||
return True, 0.5, f"Potentially dangerous methods: {', '.join(dangerous_found)}"
|
||||
|
||||
# Check if specific method test succeeded
|
||||
if payload.upper() in self.dangerous_methods:
|
||||
if response_status == 200:
|
||||
return True, 0.6, f"{payload} method accepted"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class DirectoryListingTester(BaseTester):
|
||||
"""Tester for Directory Listing exposure"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "directory_listing"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for directory listing patterns"""
|
||||
if response_status == 200:
|
||||
listing_patterns = [
|
||||
r"<title>Index of\s*/",
|
||||
r"Index of\s*/",
|
||||
r"<h1>Index of",
|
||||
r"Directory listing for\s*/",
|
||||
r"<title>Directory listing",
|
||||
r'<a\s+href="\.\./">\.\./</a>',
|
||||
r"Parent Directory</a>",
|
||||
r'\[DIR\]',
|
||||
r'\[TXT\]',
|
||||
r"<pre>.*<a href=",
|
||||
]
|
||||
|
||||
for pattern in listing_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.9, "Directory listing: Server directory contents exposed"
|
||||
|
||||
# Check for Apache/Nginx-specific listing
|
||||
if re.search(r'<address>Apache/[\d.]+ .* Server at', response_body, re.IGNORECASE):
|
||||
if "Index of" in response_body:
|
||||
return True, 0.95, "Directory listing: Apache directory listing enabled"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class DebugModeTester(BaseTester):
|
||||
"""Tester for Debug Mode/Page exposure"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "debug_mode"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for debug pages, stack traces with source paths"""
|
||||
# Werkzeug/Flask debugger
|
||||
werkzeug_patterns = [
|
||||
r"Werkzeug\s+Debugger",
|
||||
r"werkzeug\.debug",
|
||||
r"<div class=\"debugger\">",
|
||||
r"The debugger caught an exception",
|
||||
r"__debugger__",
|
||||
]
|
||||
for pattern in werkzeug_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.95, "Debug mode: Werkzeug interactive debugger exposed (RCE risk)"
|
||||
|
||||
# Laravel debug
|
||||
laravel_patterns = [
|
||||
r"Whoops!.*Laravel",
|
||||
r"Ignition\s",
|
||||
r"vendor/laravel",
|
||||
r"Laravel.*Exception",
|
||||
r"app/Http/Controllers",
|
||||
]
|
||||
for pattern in laravel_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.9, "Debug mode: Laravel debug page exposed"
|
||||
|
||||
# Django debug
|
||||
django_patterns = [
|
||||
r"You\'re seeing this error because you have <code>DEBUG = True</code>",
|
||||
r"Django Version:",
|
||||
r"Traceback.*django",
|
||||
r"INSTALLED_APPS",
|
||||
r"settings\.py",
|
||||
]
|
||||
for pattern in django_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.9, "Debug mode: Django debug page exposed"
|
||||
|
||||
# Generic stack traces with source paths
|
||||
stack_trace_patterns = [
|
||||
r"(?:File|at)\s+[\"']?(?:/[a-z]+/|C:\\)[^\s\"']+\.(?:py|php|rb|js|java|go)\b",
|
||||
r"Traceback \(most recent call last\)",
|
||||
r"Stack trace:.*(?:\.php|\.py|\.rb|\.java)",
|
||||
r"(?:Error|Exception)\s+in\s+(?:/[a-z]+/|C:\\)[^\s]+:\d+",
|
||||
]
|
||||
for pattern in stack_trace_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE | re.DOTALL):
|
||||
return True, 0.8, "Debug mode: Stack trace with source file paths exposed"
|
||||
|
||||
# ASP.NET detailed errors
|
||||
if re.search(r"Server Error in '/' Application", response_body):
|
||||
return True, 0.85, "Debug mode: ASP.NET detailed error page exposed"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ExposedAdminPanelTester(BaseTester):
|
||||
"""Tester for Publicly Accessible Admin Panel"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "exposed_admin_panel"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for admin login pages accessible publicly"""
|
||||
if response_status == 200:
|
||||
admin_panel_patterns = [
|
||||
(r"(?:admin|administrator)\s+(?:login|panel|dashboard|console)", "admin login page"),
|
||||
(r"<title>[^<]*(?:admin|dashboard|control\s*panel|cms)[^<]*</title>", "admin title"),
|
||||
(r"wp-login\.php", "WordPress login"),
|
||||
(r"wp-admin", "WordPress admin"),
|
||||
(r"/admin/login", "admin login endpoint"),
|
||||
(r"phpmyadmin", "phpMyAdmin"),
|
||||
(r"adminer\.php", "Adminer"),
|
||||
(r"cPanel", "cPanel"),
|
||||
(r"Webmin", "Webmin"),
|
||||
(r"Plesk", "Plesk"),
|
||||
(r"joomla.*administrator", "Joomla admin"),
|
||||
(r"drupal.*user/login", "Drupal admin login"),
|
||||
]
|
||||
|
||||
for pattern, panel_name in admin_panel_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.75, f"Exposed admin panel: {panel_name} accessible publicly"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ExposedApiDocsTester(BaseTester):
|
||||
"""Tester for Publicly Accessible API Documentation"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "exposed_api_docs"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for Swagger/OpenAPI documentation pages"""
|
||||
if response_status == 200:
|
||||
api_docs_patterns = [
|
||||
(r"swagger-ui", "Swagger UI"),
|
||||
(r'"swagger"\s*:\s*"[0-9.]+"', "Swagger spec"),
|
||||
(r'"openapi"\s*:\s*"[0-9.]+"', "OpenAPI spec"),
|
||||
(r"swagger-ui-bundle\.js", "Swagger UI bundle"),
|
||||
(r"<title>Swagger UI</title>", "Swagger UI page"),
|
||||
(r"redoc", "ReDoc API docs"),
|
||||
(r"api-docs", "API documentation"),
|
||||
(r"graphiql", "GraphiQL interface"),
|
||||
(r"GraphQL Playground", "GraphQL Playground"),
|
||||
(r'"paths"\s*:\s*\{', "OpenAPI paths object"),
|
||||
(r'"info"\s*:\s*\{.*"title"\s*:', "OpenAPI info object"),
|
||||
]
|
||||
|
||||
for pattern, doc_type in api_docs_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.8, f"Exposed API docs: {doc_type} publicly accessible"
|
||||
|
||||
# Check content type for JSON API specs
|
||||
content_type = response_headers.get("Content-Type", "")
|
||||
if "json" in content_type.lower():
|
||||
if re.search(r'"paths"\s*:\s*\{.*"(?:get|post|put|delete)"', response_body, re.DOTALL):
|
||||
return True, 0.85, "Exposed API docs: OpenAPI/Swagger JSON specification exposed"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class InsecureCookieFlagsTester(BaseTester):
|
||||
"""Tester for Missing Secure/HttpOnly/SameSite Cookie Flags"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "insecure_cookie_flags"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check Set-Cookie headers for missing security flags"""
|
||||
# Collect all Set-Cookie headers
|
||||
set_cookie_values = []
|
||||
for key, value in response_headers.items():
|
||||
if key.lower() == "set-cookie":
|
||||
if isinstance(value, list):
|
||||
set_cookie_values.extend(value)
|
||||
else:
|
||||
set_cookie_values.append(value)
|
||||
|
||||
if not set_cookie_values:
|
||||
return False, 0.0, None
|
||||
|
||||
issues = []
|
||||
for cookie in set_cookie_values:
|
||||
cookie_lower = cookie.lower()
|
||||
cookie_name = cookie.split("=")[0].strip()
|
||||
|
||||
# Session cookies are more critical
|
||||
is_session = any(
|
||||
s in cookie_name.lower()
|
||||
for s in ["session", "sess", "sid", "token", "auth", "jwt", "csrf"]
|
||||
)
|
||||
|
||||
missing_flags = []
|
||||
if "secure" not in cookie_lower:
|
||||
missing_flags.append("Secure")
|
||||
if "httponly" not in cookie_lower:
|
||||
missing_flags.append("HttpOnly")
|
||||
if "samesite" not in cookie_lower:
|
||||
missing_flags.append("SameSite")
|
||||
|
||||
if missing_flags:
|
||||
severity = "session cookie" if is_session else "cookie"
|
||||
issues.append(f"{cookie_name} ({severity}): missing {', '.join(missing_flags)}")
|
||||
|
||||
if issues:
|
||||
confidence = 0.8 if any("session cookie" in i for i in issues) else 0.6
|
||||
return True, confidence, f"Insecure cookie flags: {'; '.join(issues[:3])}"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class HttpSmugglingTester(BaseTester):
|
||||
"""Tester for HTTP Request Smuggling (CL/TE discrepancy)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "http_smuggling"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for CL/TE discrepancy indicators"""
|
||||
# Check for request smuggling indicators
|
||||
smuggling_indicators = [
|
||||
# Different response than expected
|
||||
(r"400 Bad Request.*(?:Content-Length|Transfer-Encoding)", "CL/TE parsing error"),
|
||||
(r"(?:invalid|malformed)\s+(?:chunk|transfer.encoding)", "chunked encoding error"),
|
||||
]
|
||||
for pattern, desc in smuggling_indicators:
|
||||
if re.search(pattern, response_body, re.IGNORECASE | re.DOTALL):
|
||||
return True, 0.7, f"HTTP smuggling indicator: {desc}"
|
||||
|
||||
# Check for dual Transfer-Encoding handling
|
||||
te_header = response_headers.get("Transfer-Encoding", "")
|
||||
cl_header = response_headers.get("Content-Length", "")
|
||||
|
||||
if te_header and cl_header:
|
||||
return True, 0.75, "HTTP smuggling: Both Transfer-Encoding and Content-Length in response"
|
||||
|
||||
# Check for timeout-based detection (context)
|
||||
if context.get("response_time_ms", 0) > 10000:
|
||||
if "transfer-encoding" in payload.lower() or "content-length" in payload.lower():
|
||||
return True, 0.6, "HTTP smuggling: Abnormal response delay with CL/TE payload"
|
||||
|
||||
# Check for response desync indicators
|
||||
if response_status == 0 or context.get("connection_reset"):
|
||||
return True, 0.65, "HTTP smuggling: Connection reset/timeout with smuggling payload"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class CachePoisoningTester(BaseTester):
|
||||
"""Tester for Web Cache Poisoning"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "cache_poisoning"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for cached response with injected unkeyed input"""
|
||||
if response_status == 200:
|
||||
# Check for cache headers indicating response was cached
|
||||
cache_indicators = {
|
||||
"X-Cache": response_headers.get("X-Cache", ""),
|
||||
"CF-Cache-Status": response_headers.get("CF-Cache-Status", ""),
|
||||
"Age": response_headers.get("Age", ""),
|
||||
"X-Varnish": response_headers.get("X-Varnish", ""),
|
||||
}
|
||||
|
||||
is_cached = False
|
||||
for header, value in cache_indicators.items():
|
||||
if value:
|
||||
if any(hit in value.upper() for hit in ["HIT", "STALE"]):
|
||||
is_cached = True
|
||||
break
|
||||
if header == "Age" and int(value or 0) > 0:
|
||||
is_cached = True
|
||||
break
|
||||
|
||||
# Check if our unkeyed input is reflected in the cached response
|
||||
if is_cached or response_headers.get("Cache-Control", ""):
|
||||
# Common unkeyed headers that might be reflected
|
||||
unkeyed_indicators = [
|
||||
r"X-Forwarded-Host", r"X-Forwarded-Scheme",
|
||||
r"X-Original-URL", r"X-Rewrite-URL",
|
||||
]
|
||||
|
||||
if payload in response_body:
|
||||
if is_cached:
|
||||
return True, 0.9, "Cache poisoning: Injected unkeyed input reflected in cached response"
|
||||
else:
|
||||
return True, 0.7, "Cache poisoning: Unkeyed input reflected - verify caching"
|
||||
|
||||
# Check for Vary header missing expected values
|
||||
vary = response_headers.get("Vary", "")
|
||||
cache_control = response_headers.get("Cache-Control", "")
|
||||
if "no-store" not in cache_control and "private" not in cache_control:
|
||||
if payload in response_body:
|
||||
return True, 0.6, "Cache poisoning potential: Input reflected in cacheable response"
|
||||
|
||||
return False, 0.0, None
|
||||
@@ -0,0 +1,443 @@
|
||||
"""
|
||||
NeuroSploit v3 - Injection Vulnerability Testers
|
||||
|
||||
Testers for XSS, SQL Injection, Command Injection, SSTI, etc.
|
||||
"""
|
||||
import re
|
||||
from typing import Tuple, Dict, Optional
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
|
||||
class XSSReflectedTester(BaseTester):
|
||||
"""Tester for Reflected XSS vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "xss_reflected"
|
||||
# Unique markers for detection
|
||||
self.markers = [
|
||||
"neurosploit",
|
||||
"xsstest123",
|
||||
"alert(1)"
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check if XSS payload is reflected in response"""
|
||||
if response_status >= 400:
|
||||
return False, 0.0, None
|
||||
|
||||
# Check if payload is reflected
|
||||
if payload in response_body:
|
||||
# Use context-aware analysis to determine execution position
|
||||
from backend.core.xss_context_analyzer import analyze_xss_execution_context
|
||||
ctx = analyze_xss_execution_context(response_body, payload)
|
||||
if ctx["executable"]:
|
||||
return True, 0.95, f"XSS payload in auto-executing context: {ctx['detail']}"
|
||||
elif ctx["interactive"]:
|
||||
return True, 0.85, f"XSS payload in interactive context: {ctx['detail']}"
|
||||
# Reflected but not in executable position
|
||||
return True, 0.5, f"XSS payload reflected but {ctx['context']}: {ctx['detail']}"
|
||||
|
||||
# Check for partial reflection (script tags, etc.)
|
||||
for marker in self.markers:
|
||||
if marker in payload and marker in response_body:
|
||||
return True, 0.6, f"XSS marker '{marker}' found in response"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class XSSStoredTester(BaseTester):
|
||||
"""Tester for Stored XSS vulnerabilities.
|
||||
|
||||
Supports two-phase verification:
|
||||
Phase 1: analyze_response() - Check if submission succeeded (data stored)
|
||||
Phase 2: analyze_display_response() - Check if payload executes on display page
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "xss_stored"
|
||||
self.storage_indicators = [
|
||||
"success", "created", "saved", "posted", "submitted",
|
||||
"thank", "comment", "added", "published", "updated",
|
||||
"your comment", "your post", "your message",
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Phase 1: Check if payload was likely stored.
|
||||
|
||||
Returns confidence 0.3-0.5 for storage-only confirmation.
|
||||
Full confirmation requires Phase 2 (analyze_display_response).
|
||||
"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Redirect after POST is a common form submission pattern
|
||||
if response_status in [301, 302, 303]:
|
||||
return True, 0.4, "Redirect after submission - payload likely stored"
|
||||
|
||||
if response_status in [200, 201]:
|
||||
# Check for storage success indicators
|
||||
for indicator in self.storage_indicators:
|
||||
if indicator in body_lower:
|
||||
return True, 0.4, f"Storage indicator found: '{indicator}'"
|
||||
|
||||
# Check if payload is reflected in the same response (immediate display)
|
||||
if payload in response_body:
|
||||
dangerous = [
|
||||
"<script", "onerror=", "onload=", "onclick=", "onfocus=",
|
||||
"onmouseover=", "<svg", "<img", "<iframe", "javascript:"
|
||||
]
|
||||
payload_lower = payload.lower()
|
||||
for ctx in dangerous:
|
||||
if ctx in payload_lower:
|
||||
return True, 0.8, f"Stored XSS: payload reflected in dangerous context ({ctx})"
|
||||
return True, 0.6, "Payload reflected in submission response"
|
||||
|
||||
# POST returning 200 often means submission accepted
|
||||
if response_status == 200 and context.get("method") == "POST":
|
||||
return True, 0.3, "POST returned 200 - submission possibly accepted"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
def analyze_display_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Phase 2: Verify payload executes on the display page.
|
||||
|
||||
Called after navigating to the page where stored content is rendered.
|
||||
"""
|
||||
if response_status >= 400:
|
||||
return False, 0.0, None
|
||||
|
||||
# Check if payload exists unescaped in display page
|
||||
if payload in response_body:
|
||||
# Use context-aware analysis to determine execution position
|
||||
from backend.core.xss_context_analyzer import analyze_xss_execution_context
|
||||
ctx = analyze_xss_execution_context(response_body, payload)
|
||||
if ctx["executable"]:
|
||||
return True, 0.95, f"Stored XSS confirmed: {ctx['detail']}"
|
||||
elif ctx["interactive"]:
|
||||
return True, 0.90, f"Stored XSS (interaction required): {ctx['detail']}"
|
||||
# Payload present but not executable
|
||||
return True, 0.5, f"Stored payload on display page but {ctx['context']}: {ctx['detail']}"
|
||||
|
||||
# Check for core execution markers even if full payload is modified
|
||||
core_markers = [
|
||||
"alert(1)", "alert(document.domain)", "onerror=alert",
|
||||
"onload=alert", "onfocus=alert", "ontoggle=alert",
|
||||
]
|
||||
body_lower = response_body.lower()
|
||||
for marker in core_markers:
|
||||
if marker in payload.lower() and marker in body_lower:
|
||||
return True, 0.85, f"Stored XSS: execution marker '{marker}' found on display page"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class XSSDomTester(BaseTester):
|
||||
"""Tester for DOM-based XSS vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "xss_dom"
|
||||
self.dom_sinks = [
|
||||
"innerHTML", "outerHTML", "document.write", "document.writeln",
|
||||
"eval(", "setTimeout(", "setInterval(", "location.href",
|
||||
"location.assign", "location.replace"
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for DOM XSS indicators"""
|
||||
# Look for dangerous DOM sinks in JavaScript
|
||||
for sink in self.dom_sinks:
|
||||
pattern = rf'{sink}[^;]*(?:location|document\.URL|document\.referrer|window\.name)'
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.7, f"Potential DOM XSS sink found: {sink}"
|
||||
|
||||
# Check if URL parameters are used in JavaScript
|
||||
if re.search(r'(?:location\.search|location\.hash|document\.URL)', response_body):
|
||||
if any(sink in response_body for sink in self.dom_sinks):
|
||||
return True, 0.6, "URL input flows to DOM sink"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class SQLiErrorTester(BaseTester):
|
||||
"""Tester for Error-based SQL Injection"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "sqli_error"
|
||||
self.error_patterns = [
|
||||
# MySQL
|
||||
r"SQL syntax.*MySQL", r"Warning.*mysql_", r"MySQLSyntaxErrorException",
|
||||
r"valid MySQL result", r"check the manual that corresponds to your MySQL",
|
||||
# PostgreSQL
|
||||
r"PostgreSQL.*ERROR", r"Warning.*pg_", r"valid PostgreSQL result",
|
||||
r"Npgsql\.", r"PG::SyntaxError",
|
||||
# SQL Server
|
||||
r"Driver.*SQL[\-\_\ ]*Server", r"OLE DB.*SQL Server",
|
||||
r"(\W|\A)SQL Server.*Driver", r"Warning.*mssql_",
|
||||
r"(\W|\A)SQL Server.*[0-9a-fA-F]{8}", r"Microsoft SQL Native Client error",
|
||||
# Oracle
|
||||
r"\bORA-[0-9][0-9][0-9][0-9]", r"Oracle error", r"Oracle.*Driver",
|
||||
r"Warning.*oci_", r"Warning.*ora_",
|
||||
# SQLite
|
||||
r"SQLite/JDBCDriver", r"SQLite\.Exception", r"System\.Data\.SQLite\.SQLiteException",
|
||||
r"Warning.*sqlite_", r"Warning.*SQLite3::",
|
||||
# Generic
|
||||
r"SQL syntax.*", r"syntax error.*SQL", r"unclosed quotation mark",
|
||||
r"quoted string not properly terminated"
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for SQL error messages in response"""
|
||||
for pattern in self.error_patterns:
|
||||
match = re.search(pattern, response_body, re.IGNORECASE)
|
||||
if match:
|
||||
return True, 0.9, f"SQL error detected: {match.group(0)[:100]}"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class SQLiUnionTester(BaseTester):
|
||||
"""Tester for Union-based SQL Injection"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "sqli_union"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for union-based SQLi indicators"""
|
||||
# Look for injected data markers
|
||||
union_markers = ["neurosploit", "uniontest", "concat(", "version()"]
|
||||
|
||||
for marker in union_markers:
|
||||
if marker in payload.lower() and marker in response_body.lower():
|
||||
return True, 0.8, f"Union injection marker '{marker}' found in response"
|
||||
|
||||
# Check for database version strings
|
||||
version_patterns = [
|
||||
r"MySQL.*\d+\.\d+", r"PostgreSQL.*\d+\.\d+",
|
||||
r"Microsoft SQL Server.*\d+", r"Oracle.*\d+",
|
||||
r"\d+\.\d+\.\d+-MariaDB"
|
||||
]
|
||||
for pattern in version_patterns:
|
||||
if re.search(pattern, response_body):
|
||||
return True, 0.7, "Database version string found - possible union SQLi"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class SQLiBlindTester(BaseTester):
|
||||
"""Tester for Boolean-based Blind SQL Injection"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "sqli_blind"
|
||||
self.baseline_length = None
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for boolean-based blind SQLi"""
|
||||
# This requires comparing responses - simplified check
|
||||
response_length = len(response_body)
|
||||
|
||||
# Check for significant difference in response
|
||||
if "baseline_length" in context:
|
||||
diff = abs(response_length - context["baseline_length"])
|
||||
if diff > 100: # Significant difference
|
||||
return True, 0.6, f"Response length differs by {diff} bytes - possible blind SQLi"
|
||||
|
||||
# Check for conditional responses
|
||||
if "1=1" in payload and response_status == 200:
|
||||
return True, 0.5, "True condition returned 200 - possible blind SQLi"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class SQLiTimeTester(BaseTester):
|
||||
"""Tester for Time-based Blind SQL Injection"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "sqli_time"
|
||||
|
||||
def check_timeout_vulnerability(self, vuln_type: str) -> bool:
|
||||
"""Time-based SQLi is indicated by timeout"""
|
||||
return True
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Time-based detection relies on timeout"""
|
||||
# Response time analysis would be done in the engine
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class CommandInjectionTester(BaseTester):
|
||||
"""Tester for OS Command Injection"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "command_injection"
|
||||
self.command_outputs = [
|
||||
# Linux
|
||||
r"root:.*:0:0:", r"bin:.*:1:1:", # /etc/passwd
|
||||
r"uid=\d+.*gid=\d+", # id command
|
||||
r"Linux.*\d+\.\d+\.\d+", # uname
|
||||
r"total \d+.*drwx", # ls -la
|
||||
# Windows
|
||||
r"Volume Serial Number",
|
||||
r"Directory of [A-Z]:\\",
|
||||
r"Windows.*\[Version",
|
||||
r"Microsoft Windows"
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for command execution evidence"""
|
||||
for pattern in self.command_outputs:
|
||||
match = re.search(pattern, response_body, re.IGNORECASE)
|
||||
if match:
|
||||
return True, 0.95, f"Command output detected: {match.group(0)[:100]}"
|
||||
|
||||
# Check for our marker
|
||||
if "neurosploit" in payload and "neurosploit" in response_body:
|
||||
return True, 0.8, "Command injection marker echoed"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class SSTITester(BaseTester):
|
||||
"""Tester for Server-Side Template Injection"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "ssti"
|
||||
# Mathematical expressions that prove code execution
|
||||
self.math_results = {
|
||||
"{{7*7}}": "49",
|
||||
"${7*7}": "49",
|
||||
"#{7*7}": "49",
|
||||
"<%= 7*7 %>": "49",
|
||||
"{{7*'7'}}": "7777777", # Jinja2
|
||||
}
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for SSTI indicators"""
|
||||
# Check mathematical results
|
||||
for expr, result in self.math_results.items():
|
||||
if expr in payload and result in response_body:
|
||||
return True, 0.95, f"SSTI confirmed: {expr} = {result}"
|
||||
|
||||
# Check for template errors
|
||||
template_errors = [
|
||||
r"TemplateSyntaxError", r"Jinja2", r"Twig_Error",
|
||||
r"freemarker\.core\.", r"velocity\.exception",
|
||||
r"org\.apache\.velocity", r"Smarty"
|
||||
]
|
||||
for pattern in template_errors:
|
||||
if re.search(pattern, response_body):
|
||||
return True, 0.7, f"Template engine error: {pattern}"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class NoSQLInjectionTester(BaseTester):
|
||||
"""Tester for NoSQL Injection"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "nosql_injection"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for NoSQL injection indicators"""
|
||||
# MongoDB errors
|
||||
nosql_errors = [
|
||||
r"MongoError", r"MongoDB", r"bson",
|
||||
r"\$where", r"\$gt", r"\$ne",
|
||||
r"SyntaxError.*JSON"
|
||||
]
|
||||
|
||||
for pattern in nosql_errors:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.7, f"NoSQL error indicator: {pattern}"
|
||||
|
||||
# Check for authentication bypass
|
||||
if "$ne" in payload or "$gt" in payload:
|
||||
if response_status == 200 and "success" in response_body.lower():
|
||||
return True, 0.6, "Possible NoSQL authentication bypass"
|
||||
|
||||
return False, 0.0, None
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
"""
|
||||
NeuroSploit v3 - Logic and Protocol Vulnerability Testers
|
||||
|
||||
Testers for race conditions, business logic, rate limiting, parameter pollution,
|
||||
type juggling, timing attacks, host header injection, HTTP smuggling, cache poisoning.
|
||||
"""
|
||||
import re
|
||||
from typing import Tuple, Dict, Optional
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
|
||||
class RaceConditionTester(BaseTester):
|
||||
"""Tester for Race Condition vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "race_condition"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for duplicate operation success indicators"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Multiple success responses from concurrent requests
|
||||
if context.get("concurrent_successes", 0) > 1:
|
||||
return True, 0.85, f"Race condition: {context['concurrent_successes']} concurrent requests succeeded"
|
||||
|
||||
# Double-spend / duplicate operation indicators
|
||||
duplicate_indicators = [
|
||||
"already processed", "duplicate", "already exists",
|
||||
"already applied", "already redeemed",
|
||||
]
|
||||
# If we got a success despite expected duplicate check
|
||||
if response_status in [200, 201]:
|
||||
success_words = ["success", "created", "processed", "applied", "completed", "confirmed"]
|
||||
if any(w in body_lower for w in success_words):
|
||||
if context.get("request_count", 0) > 1:
|
||||
return True, 0.7, "Race condition: operation succeeded multiple times"
|
||||
|
||||
# Check for resource count discrepancy
|
||||
if "balance" in body_lower or "quantity" in body_lower or "count" in body_lower:
|
||||
numbers = re.findall(r'"(?:balance|quantity|count|amount)"\s*:\s*(-?\d+\.?\d*)', response_body)
|
||||
if numbers and context.get("expected_value") is not None:
|
||||
try:
|
||||
actual = float(numbers[0])
|
||||
expected = float(context["expected_value"])
|
||||
if actual != expected:
|
||||
return True, 0.75, f"Race condition: value mismatch (expected {expected}, got {actual})"
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class BusinessLogicTester(BaseTester):
|
||||
"""Tester for Business Logic vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "business_logic"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for business logic bypass indicators"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Negative value acceptance
|
||||
if re.search(r"-\d+", payload):
|
||||
if response_status == 200:
|
||||
if any(w in body_lower for w in ["success", "accepted", "processed", "approved"]):
|
||||
return True, 0.8, "Business logic: negative value accepted"
|
||||
# Check for negative pricing
|
||||
if re.search(r'"(?:total|price|amount)"\s*:\s*-\d+', response_body):
|
||||
return True, 0.9, "Business logic: negative price/amount in response"
|
||||
|
||||
# Zero-value bypass
|
||||
if payload.strip() in ["0", "0.00", "0.0"]:
|
||||
if response_status == 200 and "success" in body_lower:
|
||||
return True, 0.75, "Business logic: zero value accepted for transaction"
|
||||
|
||||
# Workflow step skip
|
||||
if context.get("skipped_step"):
|
||||
if response_status == 200:
|
||||
return True, 0.7, f"Business logic: step '{context['skipped_step']}' was skippable"
|
||||
|
||||
# Discount/coupon abuse
|
||||
if "coupon" in payload.lower() or "discount" in payload.lower():
|
||||
if re.search(r'"discount"\s*:\s*(?:100|[1-9]\d{2,})', response_body):
|
||||
return True, 0.8, "Business logic: excessive discount applied"
|
||||
|
||||
# Role/privilege escalation via parameter
|
||||
if any(w in payload.lower() for w in ["admin", "role=admin", "is_admin=true", "privilege"]):
|
||||
if response_status == 200 and "admin" in body_lower:
|
||||
return True, 0.6, "Business logic: privilege escalation parameter accepted"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class RateLimitBypassTester(BaseTester):
|
||||
"""Tester for Rate Limit Bypass vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "rate_limit_bypass"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for continued success after many requests (bypass)"""
|
||||
headers_lower = {k.lower(): v for k, v in response_headers.items()}
|
||||
|
||||
# After many requests, still getting 200
|
||||
request_count = context.get("request_count", 0)
|
||||
if request_count > 50 and response_status == 200:
|
||||
# Check rate limit headers
|
||||
remaining = headers_lower.get("x-ratelimit-remaining",
|
||||
headers_lower.get("x-rate-limit-remaining",
|
||||
headers_lower.get("ratelimit-remaining")))
|
||||
if remaining is not None:
|
||||
try:
|
||||
if int(remaining) > 0:
|
||||
return True, 0.6, f"Rate limit not enforced after {request_count} requests (remaining: {remaining})"
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
# No rate limit headers at all
|
||||
return True, 0.7, f"No rate limiting detected after {request_count} requests"
|
||||
|
||||
# Rate limit bypass via header manipulation
|
||||
bypass_headers = ["x-forwarded-for", "x-real-ip", "x-originating-ip", "x-client-ip"]
|
||||
if any(h in payload.lower() for h in bypass_headers):
|
||||
if response_status == 200 and context.get("was_rate_limited"):
|
||||
return True, 0.85, "Rate limit bypassed via IP spoofing header"
|
||||
|
||||
# Check if 429 was expected but got 200
|
||||
if context.get("expected_429") and response_status == 200:
|
||||
return True, 0.8, "Expected 429 (rate limited) but received 200"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class ParameterPollutionTester(BaseTester):
|
||||
"""Tester for HTTP Parameter Pollution vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "parameter_pollution"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for different behavior with duplicate parameters"""
|
||||
# Compare response with baseline (single param)
|
||||
if "baseline_body" in context and "baseline_status" in context:
|
||||
baseline_len = len(context["baseline_body"])
|
||||
current_len = len(response_body)
|
||||
diff = abs(current_len - baseline_len)
|
||||
|
||||
# Significant response difference
|
||||
if diff > 200:
|
||||
return True, 0.7, f"Parameter pollution: response differs by {diff} bytes from baseline"
|
||||
|
||||
# Status code difference
|
||||
if response_status != context["baseline_status"]:
|
||||
return True, 0.75, f"Parameter pollution: status changed from {context['baseline_status']} to {response_status}"
|
||||
|
||||
# Check if attacker-controlled value was used
|
||||
if "neurosploit" in payload and "neurosploit" in response_body:
|
||||
if context.get("original_value") and context["original_value"] not in response_body:
|
||||
return True, 0.8, "Parameter pollution: attacker value used instead of original"
|
||||
|
||||
# WAF bypass via duplicate params
|
||||
if context.get("waf_blocked_original") and response_status == 200:
|
||||
return True, 0.8, "Parameter pollution: WAF bypass - blocked payload succeeded with duplicate params"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class TypeJugglingTester(BaseTester):
|
||||
"""Tester for Type Juggling / Type Coercion vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "type_juggling"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for auth bypass with type coercion"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Type juggling payloads
|
||||
juggling_values = ["0", "true", "false", "null", "[]", "{}", "0e123", "0e999"]
|
||||
|
||||
if payload.strip() in juggling_values or payload.strip().startswith("0e"):
|
||||
# Auth bypass
|
||||
if response_status == 200:
|
||||
auth_success = [
|
||||
"authenticated", "logged in", "welcome", "dashboard",
|
||||
"token", "session", "success",
|
||||
]
|
||||
for indicator in auth_success:
|
||||
if indicator in body_lower:
|
||||
return True, 0.8, f"Type juggling: auth bypass with '{payload.strip()}' - '{indicator}' in response"
|
||||
|
||||
# JWT/token accepted
|
||||
if "jwt" in body_lower or "bearer" in body_lower:
|
||||
if response_status == 200:
|
||||
return True, 0.7, f"Type juggling: token accepted with value '{payload.strip()}'"
|
||||
|
||||
# Magic hash comparison bypass (0e strings)
|
||||
if re.match(r"0e\d+", payload.strip()):
|
||||
if response_status == 200 and any(w in body_lower for w in ["match", "equal", "valid", "correct"]):
|
||||
return True, 0.85, f"Type juggling: magic hash bypass with '{payload.strip()}'"
|
||||
|
||||
# Array vs string comparison
|
||||
if payload.strip() in ["[]", "Array"]:
|
||||
if response_status == 200 and "success" in body_lower:
|
||||
return True, 0.7, "Type juggling: array comparison bypass"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class TimingAttackTester(BaseTester):
|
||||
"""Tester for Timing Attack vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "timing_attack"
|
||||
|
||||
def check_timeout_vulnerability(self, vuln_type: str) -> bool:
|
||||
"""Timing attacks are detected via response time differences"""
|
||||
return True
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check response time differences for timing side channels"""
|
||||
# Primary detection is via response timing (handled by engine)
|
||||
response_time = context.get("response_time_ms", 0)
|
||||
baseline_time = context.get("baseline_time_ms", 0)
|
||||
|
||||
if response_time > 0 and baseline_time > 0:
|
||||
diff = response_time - baseline_time
|
||||
# Significant timing difference (> 100ms)
|
||||
if diff > 100:
|
||||
return True, 0.7, f"Timing attack: {diff}ms difference (baseline: {baseline_time}ms, actual: {response_time}ms)"
|
||||
# Very significant (> 500ms)
|
||||
if diff > 500:
|
||||
return True, 0.9, f"Timing attack: {diff}ms difference strongly indicates character-by-character comparison"
|
||||
|
||||
# Check for timing via multiple measurements
|
||||
if "timing_samples" in context:
|
||||
samples = context["timing_samples"]
|
||||
if len(samples) >= 2:
|
||||
max_diff = max(samples) - min(samples)
|
||||
if max_diff > 200:
|
||||
return True, 0.65, f"Timing attack: {max_diff}ms variance across samples"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class HostHeaderInjectionTester(BaseTester):
|
||||
"""Tester for Host Header Injection vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "host_header_injection"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for Host value reflected in response links/URLs"""
|
||||
body_lower = response_body.lower()
|
||||
|
||||
# Injected host appearing in response
|
||||
evil_hosts = ["evil.com", "attacker.com", "neurosploit.test", "canary.host"]
|
||||
for host in evil_hosts:
|
||||
if host in payload.lower() and host in body_lower:
|
||||
# High confidence if in URL context
|
||||
url_contexts = [
|
||||
rf'https?://{re.escape(host)}',
|
||||
rf'href\s*=\s*["\'][^"\']*{re.escape(host)}',
|
||||
rf'action\s*=\s*["\'][^"\']*{re.escape(host)}',
|
||||
rf'redirect.*{re.escape(host)}',
|
||||
]
|
||||
for pattern in url_contexts:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.9, f"Host header injection: '{host}' reflected in URL context"
|
||||
|
||||
return True, 0.7, f"Host header injection: '{host}' reflected in response body"
|
||||
|
||||
# X-Forwarded-Host injection
|
||||
if "x-forwarded-host" in payload.lower():
|
||||
headers_lower = {k.lower(): v for k, v in response_headers.items()}
|
||||
location = headers_lower.get("location", "")
|
||||
if any(h in location.lower() for h in evil_hosts):
|
||||
return True, 0.9, "Host header injection: X-Forwarded-Host reflected in redirect"
|
||||
|
||||
# Password reset link poisoning
|
||||
if context.get("is_password_reset") and response_status == 200:
|
||||
for host in evil_hosts:
|
||||
if host in body_lower:
|
||||
return True, 0.95, f"Host header injection in password reset: link points to '{host}'"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class HttpSmugglingTester(BaseTester):
|
||||
"""Tester for HTTP Request Smuggling vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "http_smuggling"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for HTTP smuggling indicators"""
|
||||
headers_lower = {k.lower(): v for k, v in response_headers.items()}
|
||||
|
||||
# Response splitting - two HTTP responses in one
|
||||
if re.search(r"HTTP/\d\.\d\s+\d{3}", response_body):
|
||||
return True, 0.85, "HTTP smuggling: embedded HTTP response in body (response splitting)"
|
||||
|
||||
# Conflicting Content-Length and Transfer-Encoding
|
||||
has_cl = "content-length" in headers_lower
|
||||
has_te = "transfer-encoding" in headers_lower
|
||||
if has_cl and has_te:
|
||||
return True, 0.7, "HTTP smuggling: both Content-Length and Transfer-Encoding present"
|
||||
|
||||
# CL.TE or TE.CL desync indicators
|
||||
if context.get("desync_detected"):
|
||||
return True, 0.9, "HTTP smuggling: request desync confirmed"
|
||||
|
||||
# Unexpected response to smuggled second request
|
||||
if "smuggle_marker" in context:
|
||||
marker = context["smuggle_marker"]
|
||||
if marker in response_body:
|
||||
return True, 0.85, f"HTTP smuggling: smuggled request marker '{marker}' in response"
|
||||
|
||||
# Different status than expected (frontend vs backend disagreement)
|
||||
if context.get("expected_status") and response_status != context["expected_status"]:
|
||||
if response_status in [400, 403] and context["expected_status"] == 200:
|
||||
return True, 0.5, f"HTTP smuggling: status mismatch (expected {context['expected_status']}, got {response_status})"
|
||||
|
||||
# Timeout on second request (queued/poisoned)
|
||||
if context.get("second_request_timeout"):
|
||||
return True, 0.7, "HTTP smuggling: second request timed out (possible queue poisoning)"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
def check_timeout_vulnerability(self, vuln_type: str) -> bool:
|
||||
"""Smuggling can cause timeouts on subsequent requests"""
|
||||
return True
|
||||
|
||||
|
||||
class CachePoisoningTester(BaseTester):
|
||||
"""Tester for Web Cache Poisoning vulnerabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "cache_poisoning"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for cache poisoning - injected content served from cache"""
|
||||
headers_lower = {k.lower(): v for k, v in response_headers.items()}
|
||||
|
||||
# Check for cache hit with injected content
|
||||
cache_hit = False
|
||||
cache_headers = ["x-cache", "cf-cache-status", "x-varnish", "x-drupal-cache",
|
||||
"x-proxy-cache", "x-cdn-cache"]
|
||||
for header in cache_headers:
|
||||
value = headers_lower.get(header, "").lower()
|
||||
if "hit" in value:
|
||||
cache_hit = True
|
||||
break
|
||||
|
||||
# Age header indicates cached response
|
||||
age = headers_lower.get("age")
|
||||
if age and age != "0":
|
||||
cache_hit = True
|
||||
|
||||
if cache_hit:
|
||||
# Check if our injected content is in the cached response
|
||||
injection_markers = ["neurosploit", "xss", "evil.com", "attacker"]
|
||||
for marker in injection_markers:
|
||||
if marker in payload.lower() and marker in response_body.lower():
|
||||
return True, 0.9, f"Cache poisoning: injected content '{marker}' served from cache"
|
||||
|
||||
# Unkeyed header reflected in response (potential cache poison vector)
|
||||
unkeyed_headers = ["x-forwarded-host", "x-forwarded-scheme", "x-original-url",
|
||||
"x-rewrite-url", "x-forwarded-prefix"]
|
||||
for header in unkeyed_headers:
|
||||
if header in payload.lower():
|
||||
# Check if the value appears in response
|
||||
for marker in ["evil.com", "neurosploit", "attacker"]:
|
||||
if marker in payload.lower() and marker in response_body.lower():
|
||||
cache_status = "cached" if cache_hit else "uncached"
|
||||
confidence = 0.85 if cache_hit else 0.5
|
||||
return True, confidence, f"Cache poisoning: unkeyed header '{header}' reflected ({cache_status})"
|
||||
|
||||
# Cache deception check
|
||||
if context.get("is_cache_deception_test"):
|
||||
if cache_hit and ("token" in response_body.lower() or "session" in response_body.lower()):
|
||||
return True, 0.8, "Cache deception: sensitive data cached via path confusion"
|
||||
|
||||
return False, 0.0, None
|
||||
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
NeuroSploit v3 - Request Forgery Vulnerability Testers
|
||||
|
||||
Testers for SSRF and CSRF
|
||||
"""
|
||||
import re
|
||||
from typing import Tuple, Dict, Optional
|
||||
from backend.core.vuln_engine.testers.base_tester import BaseTester
|
||||
|
||||
|
||||
class SSRFTester(BaseTester):
|
||||
"""Tester for Server-Side Request Forgery"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "ssrf"
|
||||
# Cloud metadata indicators
|
||||
self.cloud_indicators = [
|
||||
r"ami-[a-z0-9]+", # AWS AMI ID
|
||||
r"instance-id",
|
||||
r"iam/security-credentials",
|
||||
r"compute/v1", # GCP
|
||||
r"metadata/instance",
|
||||
r"169\.254\.169\.254"
|
||||
]
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for SSRF indicators"""
|
||||
# Check for cloud metadata
|
||||
for pattern in self.cloud_indicators:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.95, f"SSRF to cloud metadata: {pattern}"
|
||||
|
||||
# Check for internal service indicators
|
||||
internal_indicators = [
|
||||
r"localhost",
|
||||
r"127\.0\.0\.1",
|
||||
r"192\.168\.\d+\.\d+",
|
||||
r"10\.\d+\.\d+\.\d+",
|
||||
r"172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+"
|
||||
]
|
||||
for pattern in internal_indicators:
|
||||
if pattern in payload and re.search(pattern, response_body):
|
||||
return True, 0.8, f"SSRF accessing internal resource: {pattern}"
|
||||
|
||||
# Check for different response when internal URL requested
|
||||
if response_status == 200 and len(response_body) > 100:
|
||||
if "169.254" in payload or "localhost" in payload or "127.0.0.1" in payload:
|
||||
return True, 0.6, "Response received from internal URL - possible SSRF"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class CSRFTester(BaseTester):
|
||||
"""Tester for Cross-Site Request Forgery"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "csrf"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for CSRF vulnerability indicators"""
|
||||
# Check for missing CSRF protections
|
||||
csrf_protections = [
|
||||
r'name=["\']?csrf',
|
||||
r'name=["\']?_token',
|
||||
r'name=["\']?authenticity_token',
|
||||
r'X-CSRF-TOKEN',
|
||||
r'X-XSRF-TOKEN'
|
||||
]
|
||||
|
||||
has_protection = any(
|
||||
re.search(pattern, response_body, re.IGNORECASE)
|
||||
for pattern in csrf_protections
|
||||
)
|
||||
|
||||
# Check SameSite cookie
|
||||
has_samesite = "samesite" in str(response_headers).lower()
|
||||
|
||||
# State-changing request without protection
|
||||
if not has_protection and not has_samesite:
|
||||
if response_status in [200, 302]:
|
||||
return True, 0.7, "No CSRF token found in form - possible CSRF"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class GraphqlIntrospectionTester(BaseTester):
|
||||
"""Tester for GraphQL Introspection exposure"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "graphql_introspection"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for __schema data in response indicating introspection is enabled"""
|
||||
if response_status == 200:
|
||||
# Direct __schema indicators
|
||||
schema_patterns = [
|
||||
r'"__schema"\s*:\s*\{',
|
||||
r'"__type"\s*:\s*\{',
|
||||
r'"queryType"\s*:\s*\{',
|
||||
r'"mutationType"\s*:\s*\{',
|
||||
r'"subscriptionType"\s*:',
|
||||
r'"types"\s*:\s*\[.*"name"\s*:\s*"__',
|
||||
r'"directives"\s*:\s*\[.*"name"\s*:',
|
||||
]
|
||||
|
||||
for pattern in schema_patterns:
|
||||
if re.search(pattern, response_body, re.IGNORECASE | re.DOTALL):
|
||||
return True, 0.9, "GraphQL introspection: Full schema exposed via __schema query"
|
||||
|
||||
# Check for type listings
|
||||
type_listing_patterns = [
|
||||
r'"kind"\s*:\s*"(?:OBJECT|SCALAR|ENUM|INPUT_OBJECT|INTERFACE|UNION)"',
|
||||
r'"fields"\s*:\s*\[.*"name"\s*:.*"type"\s*:',
|
||||
r'"inputFields"\s*:\s*\[',
|
||||
r'"enumValues"\s*:\s*\[',
|
||||
]
|
||||
|
||||
type_match_count = sum(
|
||||
1 for p in type_listing_patterns
|
||||
if re.search(p, response_body, re.IGNORECASE | re.DOTALL)
|
||||
)
|
||||
if type_match_count >= 2:
|
||||
return True, 0.85, "GraphQL introspection: Type schema data exposed"
|
||||
|
||||
# Check for field suggestions (partial introspection)
|
||||
if re.search(r'"(?:message|errors)".*"Did you mean.*"', response_body):
|
||||
return True, 0.6, "GraphQL introspection: Field suggestions leak schema information"
|
||||
|
||||
return False, 0.0, None
|
||||
|
||||
|
||||
class GraphqlDosTester(BaseTester):
|
||||
"""Tester for GraphQL Denial of Service via deeply nested queries"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "graphql_dos"
|
||||
|
||||
def analyze_response(
|
||||
self,
|
||||
payload: str,
|
||||
response_status: int,
|
||||
response_headers: Dict,
|
||||
response_body: str,
|
||||
context: Dict
|
||||
) -> Tuple[bool, float, Optional[str]]:
|
||||
"""Check for slow response with deeply nested queries indicating DoS potential"""
|
||||
# Check response time for nested query DoS
|
||||
response_time_ms = context.get("response_time_ms", 0)
|
||||
|
||||
# Nested query indicators in payload
|
||||
nesting_indicators = [
|
||||
payload.count("{") > 5,
|
||||
"__typename" in payload and payload.count("__typename") > 3,
|
||||
"fragment" in payload.lower() and "..." in payload,
|
||||
]
|
||||
is_nested_payload = any(nesting_indicators)
|
||||
|
||||
if is_nested_payload:
|
||||
# Very slow response indicates resource exhaustion
|
||||
if response_time_ms > 10000: # > 10 seconds
|
||||
return True, 0.85, f"GraphQL DoS: Deeply nested query caused {response_time_ms}ms response time"
|
||||
|
||||
if response_time_ms > 5000: # > 5 seconds
|
||||
return True, 0.7, f"GraphQL DoS: Nested query caused slow response ({response_time_ms}ms)"
|
||||
|
||||
# Check for timeout/error responses
|
||||
if response_status in [408, 504, 502]:
|
||||
if is_nested_payload:
|
||||
return True, 0.8, "GraphQL DoS: Server timeout on deeply nested query"
|
||||
|
||||
# Check for resource limit errors (server has some protection but confirms issue)
|
||||
resource_errors = [
|
||||
r"query.*(?:too complex|too deep|exceeds.*(?:depth|complexity))",
|
||||
r"max.*(?:depth|complexity).*(?:exceeded|reached)",
|
||||
r"(?:depth|complexity)\s+limit",
|
||||
r"query.*(?:cost|weight).*exceeded",
|
||||
]
|
||||
for pattern in resource_errors:
|
||||
if re.search(pattern, response_body, re.IGNORECASE):
|
||||
return True, 0.5, "GraphQL DoS: Depth/complexity limits exist but confirm nested queries are processed"
|
||||
|
||||
# Server error on complex query
|
||||
if response_status == 500 and is_nested_payload:
|
||||
return True, 0.65, "GraphQL DoS: Server error on deeply nested query"
|
||||
|
||||
return False, 0.0, None
|
||||
Reference in New Issue
Block a user