Compare commits

...

2 Commits

Author SHA1 Message Date
CyberMind
72b7eca12e
Merge pull request #606 from CyberMind-FR/feature/605-waf-refuse-unmapped-hosts-close-open-for
Some checks are pending
License Headers / check (push) Waiting to run
fix(waf): refuse unmapped hosts — close open forward-proxy (loops + 72% errors)
2026-06-15 17:19:25 +02:00
0d1c49307e fix(waf): refuse unmapped hosts — close open forward-proxy (closes #605)
In --mode regular the addon relayed any Host; HAProxy default_backend made
the WAF an open forward proxy abused by scanners (~72% error churn + 11
loop-508s/hr). requestheaders now serves ONLY our vhosts (routes / our
domains via routes-derived local_suffixes -> nginx 9080 / SELF_HOSTS) and
returns 421 otherwise with no upstream connect. Applied to both synced
secubox_waf.py copies. Verified live: 0 external server-connects, 0 loops,
apt/admin/kbin 200, scanners 421.
2026-06-15 17:19:06 +02:00
4 changed files with 91 additions and 12 deletions

View File

@ -712,6 +712,12 @@ class SecuBoxWAF:
if ROUTES_FILE.exists():
try:
self.routes = json.loads(ROUTES_FILE.read_text())
sfx = set()
for _h in self.routes:
_p = _h.split('.')
if len(_p) >= 2 and not _p[-1].isdigit():
sfx.add('.'.join(_p[-2:]))
self.local_suffixes = sfx
ctx.log.info(f"Loaded {len(self.routes)} routes")
except Exception as e:
ctx.log.error(f"Failed to load routes: {e}")
@ -920,12 +926,12 @@ class SecuBoxWAF:
ctx.log.warn(f"BAN FAILED for {ip} ({reason}) : LAPI off + cscli unavailable")
def requestheaders(self, flow: http.HTTPFlow):
# #603 — mitmproxy 11 opens the upstream connection BETWEEN the
# requestheaders and request hooks, so upstream redirection must
# happen here. Doing it in request() (below) is too late: the
# socket is already connected to the original Host, so routed
# vhosts went to their public DNS IP instead of the internal
# backend. Setting flow.server_conn.address here fixes routing.
# #605 — mitmproxy 11 opens the upstream connection before request(),
# so routing must happen here. ALSO: in --mode regular mitmproxy is a
# forward proxy that would relay ANY Host, so internet scanners abused
# it as an open proxy (~70% error churn + self-loops). Serve ONLY our
# own vhosts: mapped (routes), our domains (-> nginx catch-all), or our
# own IPs; refuse everything else with 421 and never open an upstream.
try:
host = flow.request.pretty_host
if host in self.routes:
@ -938,9 +944,32 @@ class SecuBoxWAF:
except Exception:
pass
flow.request.headers['Host'] = orig
return
if host in SELF_HOSTS or self._is_local_host(host):
flow.request.host = '192.168.1.200'
flow.request.port = 9080
try:
flow.server_conn.address = ('192.168.1.200', 9080)
except Exception:
pass
return
self.stats['blocked'] = self.stats.get('blocked', 0) + 1
flow.response = http.Response.make(
421,
b'<h1>421 Misdirected Request</h1><p>SecuBox WAF does not proxy this host.</p>',
{'Content-Type': 'text/html', 'X-SecuBox-WAF': 'unmapped-host'},
)
except Exception as e:
ctx.log.warn(f'[requestheaders-route] {e}')
def _is_local_host(self, host: str) -> bool:
# #605 — is `host` one of our own (registrable) domains? Derived from
# the routed hosts in load_routes (self.local_suffixes).
sfx = getattr(self, 'local_suffixes', None)
if not sfx:
return False
return any(host == s or host.endswith('.' + s) for s in sfx)
def request(self, flow: http.HTTPFlow):
# Connection close (Phase 6.J leak fix, ref #496) — prevents mitmproxy
# from accumulating idle keep-alive sockets to upstream backends.

View File

@ -1,3 +1,15 @@
secubox-mitmproxy (1.0.6-1~bookworm1) bookworm; urgency=medium
* fix(waf): refuse unmapped hosts — close the open forward-proxy (#605). In
--mode regular the addon relayed any Host, so HAProxy's default_backend
made the WAF an open proxy; internet scanners drove ~72% backend-error
churn + self-loops. The `requestheaders` hook now serves ONLY our vhosts
(mapped, our domains via routes-derived `local_suffixes` → nginx catch-all,
or our own IPs) and returns 421 for everything else with no upstream
connect. Verified live: 0 external server-connects, 0 loop-508s.
-- Gerald KERMA <devel@cybermind.fr> Mon, 15 Jun 2026 16:30:00 +0200
secubox-mitmproxy (1.0.5-1~bookworm1) bookworm; urgency=medium
* fix(waf): mitmproxy-11 upstream routing (#603). The addon only redirected

View File

@ -1,3 +1,12 @@
secubox-waf (1.2.4-1~bookworm1) bookworm; urgency=medium
* fix(waf): refuse unmapped hosts in the addon copy — close the open
forward-proxy (#605, synced with secubox-mitmproxy). 421 for any host not
in routes / our domains / our IPs; no upstream connect. Kills the ~72%
scanner-driven error churn and the self-loop 508s.
-- Gerald KERMA <devel@cybermind.fr> Mon, 15 Jun 2026 16:30:00 +0200
secubox-waf (1.2.3-1~bookworm1) bookworm; urgency=medium
* fix(waf): mitmproxy-11 upstream routing — `requestheaders` hook in the

View File

@ -594,6 +594,12 @@ class SecuBoxWAF:
if ROUTES_FILE.exists():
try:
self.routes = json.loads(ROUTES_FILE.read_text())
sfx = set()
for _h in self.routes:
_p = _h.split('.')
if len(_p) >= 2 and not _p[-1].isdigit():
sfx.add('.'.join(_p[-2:]))
self.local_suffixes = sfx
ctx.log.info(f"Loaded {len(self.routes)} routes")
except Exception as e:
ctx.log.error(f"Failed to load routes: {e}")
@ -776,12 +782,12 @@ class SecuBoxWAF:
ctx.log.warn(f"BAN FAILED for {ip} ({reason})")
def requestheaders(self, flow: http.HTTPFlow):
# #603 — mitmproxy 11 opens the upstream connection BETWEEN the
# requestheaders and request hooks, so upstream redirection must
# happen here. Doing it in request() (below) is too late: the
# socket is already connected to the original Host, so routed
# vhosts went to their public DNS IP instead of the internal
# backend. Setting flow.server_conn.address here fixes routing.
# #605 — mitmproxy 11 opens the upstream connection before request(),
# so routing must happen here. ALSO: in --mode regular mitmproxy is a
# forward proxy that would relay ANY Host, so internet scanners abused
# it as an open proxy (~70% error churn + self-loops). Serve ONLY our
# own vhosts: mapped (routes), our domains (-> nginx catch-all), or our
# own IPs; refuse everything else with 421 and never open an upstream.
try:
host = flow.request.pretty_host
if host in self.routes:
@ -794,9 +800,32 @@ class SecuBoxWAF:
except Exception:
pass
flow.request.headers['Host'] = orig
return
if host in SELF_HOSTS or self._is_local_host(host):
flow.request.host = '192.168.1.200'
flow.request.port = 9080
try:
flow.server_conn.address = ('192.168.1.200', 9080)
except Exception:
pass
return
self.stats['blocked'] = self.stats.get('blocked', 0) + 1
flow.response = http.Response.make(
421,
b'<h1>421 Misdirected Request</h1><p>SecuBox WAF does not proxy this host.</p>',
{'Content-Type': 'text/html', 'X-SecuBox-WAF': 'unmapped-host'},
)
except Exception as e:
ctx.log.warn(f'[requestheaders-route] {e}')
def _is_local_host(self, host: str) -> bool:
# #605 — is `host` one of our own (registrable) domains? Derived from
# the routed hosts in load_routes (self.local_suffixes).
sfx = getattr(self, 'local_suffixes', None)
if not sfx:
return False
return any(host == s or host.endswith('.' + s) for s in sfx)
def request(self, flow: http.HTTPFlow):
# Connection close (Phase 6.J leak fix, ref #496) — prevents mitmproxy
# from accumulating idle keep-alive sockets to upstream backends.