Compare commits

...

3 Commits

3 changed files with 73 additions and 0 deletions

View File

@ -1,3 +1,12 @@
secubox-toolbox (2.6.49-1~bookworm1) bookworm; urgency=medium
* Banner fix (#639): inject the transparency banner only into top-level
documents (Sec-Fetch-Dest: document, or absent for old UAs) — skip iframes
/ sub-documents / sub-resources so a visit gets ONE banner, not one per
frame. Gates both the stream_inject and legacy buffer paths.
-- Gerald KERMA <devel@cybermind.fr> Wed, 17 Jun 2026 21:00:00 +0200
secubox-toolbox (2.6.48-1~bookworm1) bookworm; urgency=medium secubox-toolbox (2.6.48-1~bookworm1) bookworm; urgency=medium
* R3 banner fix (#636): the mitm now serves /__toolbox/loader.js + * R3 banner fix (#636): the mitm now serves /__toolbox/loader.js +

View File

@ -404,6 +404,14 @@ def _detect_csp_strict(flow: http.HTTPFlow) -> bool:
return False return False
def _is_top_level_document(flow: http.HTTPFlow) -> bool:
"""True if the response is a top-level navigation (so the banner belongs).
Skip iframes/sub-documents/sub-resources so we inject ONE banner per visit.
Sec-Fetch-Dest absent (old UAs) assume top-level (best-effort, prior behavior)."""
dest = (flow.request.headers.get("sec-fetch-dest", "") or "").lower()
return dest in ("", "document")
# Per-level visual theme (#545). R3 — and the planned R4 — get the # Per-level visual theme (#545). R3 — and the planned R4 — get the
# neon-tube treatment (dark glass bar, glowing tube border + neon # neon-tube treatment (dark glass bar, glowing tube border + neon
# text-shadow). R2 keeps the original amber flat bar. All values are inline # text-shadow). R2 keeps the original amber flat bar. All values are inline
@ -800,6 +808,10 @@ class InjectBanner:
# that survives strict CSP. # that survives strict CSP.
if _detect_csp_strict(flow): if _detect_csp_strict(flow):
return return
# #639 — only inject into top-level navigations; iframes/sub-documents
# each get their own responseheaders call → multiple banners per visit.
if not _is_top_level_document(flow):
return
try: try:
resp.stream = _LoaderInjector(_loader_script(flow)) resp.stream = _LoaderInjector(_loader_script(flow))
flow.metadata["sbx_streamed"] = True flow.metadata["sbx_streamed"] = True
@ -832,6 +844,10 @@ class InjectBanner:
return return
except Exception: except Exception:
pass pass
# #639 — only inject into top-level navigations; iframes/sub-documents
# share the same buffer path and would each get a banner.
if not _is_top_level_document(flow):
return
# Phase 10 perf : cheap pre-flight check on Content-Length to avoid # Phase 10 perf : cheap pre-flight check on Content-Length to avoid
# reading multi-MB bodies into RAM just to discover we'd skip them. # reading multi-MB bodies into RAM just to discover we'd skip them.
# `flow.response.content` would buffer the whole body before returning. # `flow.response.content` would buffer the whole body before returning.

View File

@ -0,0 +1,48 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
import sys, pathlib, importlib, json
ADDON_DIR = pathlib.Path(__file__).resolve().parents[1] / "mitmproxy_addons"
sys.path.insert(0, str(ADDON_DIR))
from mitmproxy.test import tflow, tutils # noqa: E402
from secubox_toolbox import filters # noqa: E402
def _addon(monkeypatch, tmp_path):
fp = tmp_path / "filters.json"
fp.write_text(json.dumps({"banner": True, "stream_inject": True}))
monkeypatch.setattr(filters, "FILTERS_PATH", str(fp))
filters.get_filters(force=True)
import inject_banner
importlib.reload(inject_banner)
monkeypatch.setattr(inject_banner, "_client_level", lambda flow: "r3")
return inject_banner
def _html(dest=None):
f = tflow.tflow(resp=tutils.tresp())
f.response.headers["content-type"] = "text/html; charset=utf-8"
f.response.status_code = 200
if dest is not None:
f.request.headers["sec-fetch-dest"] = dest
return f
def test_iframe_not_streamed(monkeypatch, tmp_path):
ib = _addon(monkeypatch, tmp_path)
f = _html(dest="iframe")
ib.InjectBanner().responseheaders(f)
assert not f.metadata.get("sbx_streamed") # iframe → no banner
def test_document_streamed(monkeypatch, tmp_path):
ib = _addon(monkeypatch, tmp_path)
f = _html(dest="document")
ib.InjectBanner().responseheaders(f)
assert f.metadata.get("sbx_streamed") is True # top-level → banner
def test_missing_dest_streamed(monkeypatch, tmp_path):
ib = _addon(monkeypatch, tmp_path)
f = _html(dest=None)
ib.InjectBanner().responseheaders(f)
assert f.metadata.get("sbx_streamed") is True # absent → assume top-level