From 0d1c49307e3f4bbf7d03965d6d10bf6f7d27d046 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 15 Jun 2026 17:19:06 +0200 Subject: [PATCH] =?UTF-8?q?fix(waf):=20refuse=20unmapped=20hosts=20?= =?UTF-8?q?=E2=80=94=20close=20open=20forward-proxy=20(closes=20#605)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../secubox-mitmproxy/addons/secubox_waf.py | 41 ++++++++++++++++--- packages/secubox-mitmproxy/debian/changelog | 12 ++++++ packages/secubox-waf/debian/changelog | 9 ++++ packages/secubox-waf/mitmproxy/secubox_waf.py | 41 ++++++++++++++++--- 4 files changed, 91 insertions(+), 12 deletions(-) diff --git a/packages/secubox-mitmproxy/addons/secubox_waf.py b/packages/secubox-mitmproxy/addons/secubox_waf.py index b8c27670..89cd087e 100644 --- a/packages/secubox-mitmproxy/addons/secubox_waf.py +++ b/packages/secubox-mitmproxy/addons/secubox_waf.py @@ -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'

421 Misdirected Request

SecuBox WAF does not proxy this host.

', + {'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. diff --git a/packages/secubox-mitmproxy/debian/changelog b/packages/secubox-mitmproxy/debian/changelog index a96c6af4..1add1edc 100644 --- a/packages/secubox-mitmproxy/debian/changelog +++ b/packages/secubox-mitmproxy/debian/changelog @@ -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 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 diff --git a/packages/secubox-waf/debian/changelog b/packages/secubox-waf/debian/changelog index 6278642a..033218a4 100644 --- a/packages/secubox-waf/debian/changelog +++ b/packages/secubox-waf/debian/changelog @@ -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 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 diff --git a/packages/secubox-waf/mitmproxy/secubox_waf.py b/packages/secubox-waf/mitmproxy/secubox_waf.py index 61dd29a9..bf8c199d 100644 --- a/packages/secubox-waf/mitmproxy/secubox_waf.py +++ b/packages/secubox-waf/mitmproxy/secubox_waf.py @@ -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'

421 Misdirected Request

SecuBox WAF does not proxy this host.

', + {'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.