Compare commits

...

2 Commits

Author SHA1 Message Date
CyberMind
6160ec9aaa
Merge pull request #654 from CyberMind-FR/fix/653-fix-toolbox-banner-reliability-inline-bu
Some checks are pending
License Headers / check (push) Waiting to run
fix(toolbox): banner reliability — inline bundle (kill connect-src fetch) + SPA re-assert
2026-06-18 12:14:33 +02:00
f30b3b39f8 fix(toolbox #653): inline bundle into loader (kill connect-src fetch, faster) + SPA re-assert 2026-06-18 12:11:47 +02:00
5 changed files with 99 additions and 9 deletions

View File

@ -1,3 +1,18 @@
secubox-toolbox (2.6.56-1~bookworm1) bookworm; urgency=medium
* fix(#653): banner reliability — the injected loader now carries the
per-client bundle inline (`data-bundle` attr) and renders WITHOUT a second
`fetch('/__toolbox/bundle')`: faster (one less round-trip) and no
`connect-src` CSP dependency. Fetch retained as back-compat fallback.
* fix(#653): loader re-asserts the banner across SPA navigations
(pushState/replaceState/popstate) and if the DOM drops it — render() stays
idempotent (guards on #sbx-banner). NOTE: genuinely strict-CSP sites
(script-src 'none' / strict style-src) still can't be served from the page
without weakening their CSP — that's the browser-extension's job (separate),
we deliberately do NOT rewrite site CSP headers.
-- Gerald KERMA <devel@cybermind.fr> Thu, 18 Jun 2026 16:30:00 +0200
secubox-toolbox (2.6.55-1~bookworm1) bookworm; urgency=medium
* perf(#651): broaden the media SNI-splice seed (11 → 35 video/image/audio

View File

@ -702,8 +702,20 @@ def _stream_enabled() -> bool:
return False
def _attr_escape(s: str) -> str:
"""Escape a string for an HTML double-quoted attribute value."""
return (s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
.replace('"', "&quot;").replace("'", "&#39;"))
def _loader_script(flow) -> bytes:
"""Tiny loader <script> tag carrying the client identity + WG flag."""
"""Tiny loader <script> tag carrying the client identity + WG flag.
#653 — inline the per-client bundle as a data- attribute so the loader
renders the banner WITHOUT a second `fetch('/__toolbox/bundle')`: faster
(one less round-trip) and no `connect-src` CSP dependency. Best-effort; the
loader falls back to the fetch path when the attribute is absent.
"""
mh, wg = "", "0"
try:
ip = flow.client_conn.peername[0] if flow.client_conn.peername else None
@ -713,8 +725,17 @@ def _loader_script(flow) -> bytes:
mh = mac_hash_of(ip) or ""
except Exception:
pass
tag = ('<script src="/__toolbox/loader.js" data-mh="%s" data-wg="%s" async></script>'
% (mh, wg))
bundle_attr = ""
if _HAS_LEVEL:
try:
import json as _json
from secubox_toolbox import bundle as _b
j = _json.dumps(_b.get_bundle(mh, wg == "1"), separators=(",", ":"))
bundle_attr = ' data-bundle="%s"' % _attr_escape(j)
except Exception:
bundle_attr = ""
tag = ('<script src="/__toolbox/loader.js" data-mh="%s" data-wg="%s"%s async></script>'
% (mh, wg, bundle_attr))
return b"<!-- " + _GUARD + b" -->" + tag.encode("ascii", "ignore")

View File

@ -150,9 +150,31 @@ LOADER_JS = r"""(function(){
var btn = bar.querySelector("button");
if (btn) btn.onclick = function(){ try { document.body.style.paddingTop = ""; } catch (_) {} bar.remove(); };
}
fetch("/__toolbox/bundle?mh=" + encodeURIComponent(mh) + "&wg=" + encodeURIComponent(wg), {credentials:"omit"})
.then(function(r){ return r.json(); })
.then(function(b){ ready(function(){ render(b); }); })
.catch(function(){});
// #653 — re-assert the banner across SPA navigations / DOM wipes. render()
// is idempotent (guards on #sbx-banner), so re-calling is safe.
function setupReassert(b){
if (window.__SBX_REASSERT__) return; window.__SBX_REASSERT__ = 1;
function reassert(){ ready(function(){ if (!document.getElementById("sbx-banner")) render(b); }); }
["pushState","replaceState"].forEach(function(m){
var orig = history[m];
if (typeof orig === "function") {
history[m] = function(){ var r = orig.apply(this, arguments); setTimeout(reassert, 60); return r; };
}
});
window.addEventListener("popstate", function(){ setTimeout(reassert, 60); });
}
function go(b){ if (!b) return; ready(function(){ render(b); }); setupReassert(b); }
// #653 — prefer the inlined bundle (no fetch → faster, no connect-src dep);
// fall back to the fetch when the data- attribute is absent (back-compat).
var inb = null;
try { if (ds.bundle) inb = JSON.parse(ds.bundle); } catch (_) { inb = null; }
if (inb) {
go(inb);
} else {
fetch("/__toolbox/bundle?mh=" + encodeURIComponent(mh) + "&wg=" + encodeURIComponent(wg), {credentials:"omit"})
.then(function(r){ return r.json(); })
.then(go)
.catch(function(){});
}
})();
"""

View File

@ -0,0 +1,27 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
import sys, pathlib, importlib, re
ADDON_DIR = pathlib.Path(__file__).resolve().parents[1] / "mitmproxy_addons"
sys.path.insert(0, str(ADDON_DIR))
from mitmproxy.test import tflow # noqa: E402
def test_attr_escape():
import inject_banner
assert inject_banner._attr_escape('a"b<c>&\'') == 'a&quot;b&lt;c&gt;&amp;&#39;'
def test_loader_script_inlines_bundle():
import inject_banner; importlib.reload(inject_banner)
out = inject_banner._loader_script(tflow.tflow()).decode("ascii", "ignore")
assert 'src="/__toolbox/loader.js"' in out
assert 'data-bundle="' in out # bundle inlined (no fetch needed)
m = re.search(r'data-bundle="([^"]*)"', out)
assert m and "&quot;" in m.group(1) # JSON quotes attr-escaped
def test_loader_js_uses_inline_then_fetch_fallback():
from secubox_toolbox import bundle
js = bundle.LOADER_JS
assert "ds.bundle" in js and "JSON.parse" in js # reads inlined bundle
assert "setupReassert" in js # SPA re-assert present
assert 'fetch("/__toolbox/bundle' in js # fetch retained as fallback

View File

@ -48,6 +48,11 @@ def test_get_bundle_caches(monkeypatch):
def test_loader_js_is_served_string():
assert "addEventListener" not in bundle.LOADER_JS # uses currentScript pattern
assert "__toolbox/bundle" in bundle.LOADER_JS
# Initial render must NOT depend on a load/DOMContentLoaded event — it uses
# currentScript + ready() polling so it works even when injected mid-stream.
# (#653: the popstate listener is for SPA re-assert, not initial render, so a
# blanket "no addEventListener" ban is too broad — assert the real intent.)
assert "DOMContentLoaded" not in bundle.LOADER_JS
assert 'addEventListener("load"' not in bundle.LOADER_JS
assert "__toolbox/bundle" in bundle.LOADER_JS # fetch retained as fallback
assert bundle.LOADER_JS.strip().startswith("(function()")