mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-29 15:31:31 +00:00
Compare commits
6 Commits
c46e24f820
...
72e8cbd2db
| Author | SHA1 | Date | |
|---|---|---|---|
| 72e8cbd2db | |||
| 827165e6fd | |||
| 0d906b1471 | |||
| d1607328fd | |||
| aae47c6e2e | |||
| 4329ab2d7b |
|
|
@ -29,6 +29,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -471,6 +472,26 @@ func (px *Proxy) mitmPipeline(tconn *tls.Conn, rawClient net.Conn, host, verdict
|
||||||
if px.cspDemo {
|
if px.cspDemo {
|
||||||
cspNonce, cspBypassed = relaxCSPForLoader(resp.Header)
|
cspNonce, cspBypassed = relaxCSPForLoader(resp.Header)
|
||||||
}
|
}
|
||||||
|
// CSP diagnostic (#751) — opt-in via SBX_DEBUG_CSP, off by default (zero cost
|
||||||
|
// when unset). For every injected HTML response it logs what relaxCSPForLoader
|
||||||
|
// actually saw — the proto, the count of CSP / CSP-Report-Only headers visible
|
||||||
|
// in resp.Header, the borrowed nonce and the bypass decision. Kept as a
|
||||||
|
// permanent operator tool: it pinpoints why a banner does/doesn't render on a
|
||||||
|
// given site (header present? nonce-source? hash-only? strict-dynamic?), the
|
||||||
|
// class of problem that took an x.com-shaped CSP to surface.
|
||||||
|
if os.Getenv("SBX_DEBUG_CSP") != "" {
|
||||||
|
csps := resp.Header.Values("Content-Security-Policy")
|
||||||
|
cspRO := resp.Header.Values("Content-Security-Policy-Report-Only")
|
||||||
|
head := ""
|
||||||
|
if len(csps) > 0 {
|
||||||
|
head = csps[0]
|
||||||
|
if len(head) > 220 {
|
||||||
|
head = head[:220]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("[csp-debug] host=%s proto=%s status=%d cspHdrs=%d cspRO=%d nonce=%q bypassed=%v head=%q",
|
||||||
|
host, resp.Proto, resp.StatusCode, len(csps), len(cspRO), cspNonce, cspBypassed, head)
|
||||||
|
}
|
||||||
// #662 — INLINE the banner (supersedes the <script src="/__toolbox/loader.js">
|
// #662 — INLINE the banner (supersedes the <script src="/__toolbox/loader.js">
|
||||||
// tag): sites with a SERVICE WORKER hijack the same-origin src before it
|
// tag): sites with a SERVICE WORKER hijack the same-origin src before it
|
||||||
// reaches this engine. We fetch the COMPLETE script body from the portal
|
// reaches this engine. We fetch the COMPLETE script body from the portal
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,14 @@
|
||||||
|
secubox-toolbox-ng (0.1.23-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* #751 rebuild from master: the deployed 0.1.22 binary was STALE and lacked
|
||||||
|
the working #728 nonce-CSP borrow, so the transparency banner was blocked on
|
||||||
|
nonce-CSP sites (x.com) — the inline <script> got no nonce. A fresh master
|
||||||
|
build restores the nonce-borrow (banner renders on x.com + news). Adds the
|
||||||
|
SBX_DEBUG_CSP opt-in diagnostic (logs per-injected-response CSP visibility +
|
||||||
|
borrow decision; off by default, zero cost when unset).
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sat, 27 Jun 2026 06:30:00 +0000
|
||||||
|
|
||||||
secubox-toolbox-ng (0.1.22-1~bookworm1) bookworm; urgency=medium
|
secubox-toolbox-ng (0.1.22-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* media catcher: ship a tmpfiles.d entry so /run/secubox/media-catch.jsonl is
|
* media catcher: ship a tmpfiles.d entry so /run/secubox/media-catch.jsonl is
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,13 @@
|
||||||
|
secubox-toolbox (2.7.21-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* #754 reconcile bundle.py to the working #740 DOM-API banner (mk() builder,
|
||||||
|
Trusted-Types-proof — renders on x.com/news/strict-CSP) + the R4 tier folded
|
||||||
|
into its level switch + the #752 top-frame guard (banner never renders in a
|
||||||
|
3rd-party iframe, e.g. the Dailymotion player on leparisien). Closes the
|
||||||
|
deployed-but-unmerged feature/740 banner drift. Reviewed; 179 toolbox tests.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sat, 27 Jun 2026 06:00:00 +0000
|
||||||
|
|
||||||
secubox-toolbox (2.7.20-1~bookworm1) bookworm; urgency=medium
|
secubox-toolbox (2.7.20-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* R4 analyst tier (#736): add R4 to the banner topbar level switch
|
* R4 analyst tier (#736): add R4 to the banner topbar level switch
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,15 @@ def _tor_mode() -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _ad_guard() -> bool:
|
||||||
|
"""Master ad-block switch on? (#740) Read from filters; default on."""
|
||||||
|
try:
|
||||||
|
from .filters import get_filters
|
||||||
|
return bool(get_filters().get("ad_guard", True))
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def build_bundle(client_id: str, is_wg: bool = False) -> dict:
|
def build_bundle(client_id: str, is_wg: bool = False) -> dict:
|
||||||
"""Build the per-client cosmetic decision bundle (pure given inputs + pin file)."""
|
"""Build the per-client cosmetic decision bundle (pure given inputs + pin file)."""
|
||||||
return {
|
return {
|
||||||
|
|
@ -91,6 +100,7 @@ def build_bundle(client_id: str, is_wg: bool = False) -> dict:
|
||||||
"report_url": _report_url(client_id, is_wg),
|
"report_url": _report_url(client_id, is_wg),
|
||||||
"tracker_patterns": TRACKER_PATTERNS,
|
"tracker_patterns": TRACKER_PATTERNS,
|
||||||
"tor_mode": _tor_mode(),
|
"tor_mode": _tor_mode(),
|
||||||
|
"ad_guard": _ad_guard(),
|
||||||
"ts": int(time.time()),
|
"ts": int(time.time()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,6 +113,12 @@ def invalidate(client_id: str) -> None:
|
||||||
_cache.pop(k, None)
|
_cache.pop(k, None)
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_all() -> None:
|
||||||
|
"""#740 — drop ALL cached bundles after a GLOBAL filter change (ad_guard /
|
||||||
|
tor_mode toggled from the banner) so every client picks up the new state."""
|
||||||
|
_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
def get_bundle(client_id: str, is_wg: bool = False) -> dict:
|
def get_bundle(client_id: str, is_wg: bool = False) -> dict:
|
||||||
"""Return the cached bundle for a client, rebuilding past the TTL. Fail-open."""
|
"""Return the cached bundle for a client, rebuilding past the TTL. Fail-open."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -146,6 +162,13 @@ def get_bundle(client_id: str, is_wg: bool = False) -> dict:
|
||||||
# + 2s poll; the prelude calls ensure() (inline) or sets `bundle` then ensure()s
|
# + 2s poll; the prelude calls ensure() (inline) or sets `bundle` then ensure()s
|
||||||
# (src-loader).
|
# (src-loader).
|
||||||
_BANNER_CORE = r"""
|
_BANNER_CORE = r"""
|
||||||
|
// #752 — top-frame ONLY: never render inside an embedded sub-frame (3rd-party
|
||||||
|
// video players e.g. geo.dailymotion.com, ad/consent iframes). A same-origin
|
||||||
|
// iframe trips window.top !== window.self; a cross-origin one throws on the
|
||||||
|
// access → both bail. The transparency banner belongs on the top document,
|
||||||
|
// once. Returns from the IIFE before any function runs (ensure() is appended
|
||||||
|
// after this block by both the inline and src-loader preludes).
|
||||||
|
try { if (window.top !== window.self) return; } catch (_) { return; }
|
||||||
function ready(fn){ if (document.body) { fn(); } else { setTimeout(function(){ready(fn);}, 30); } }
|
function ready(fn){ if (document.body) { fn(); } else { setTimeout(function(){ready(fn);}, 30); } }
|
||||||
function esc(t){ return String(t).replace(/[&<>"]/g, function(c){
|
function esc(t){ return String(t).replace(/[&<>"]/g, function(c){
|
||||||
return {"&":"&","<":"<",">":">","\"":"""}[c]; }); }
|
return {"&":"&","<":"<",">":">","\"":"""}[c]; }); }
|
||||||
|
|
@ -174,19 +197,34 @@ _BANNER_CORE = r"""
|
||||||
// #724 — inline R0..R3 level switch. Shows the real current level (highlighted)
|
// #724 — inline R0..R3 level switch. Shows the real current level (highlighted)
|
||||||
// and lets the client change it: GET /__toolbox/set-level (same-origin, the Go
|
// and lets the client change it: GET /__toolbox/set-level (same-origin, the Go
|
||||||
// engine reverse-proxies it to the portal), then reload so the new tier applies.
|
// engine reverse-proxies it to the portal), then reload so the new tier applies.
|
||||||
|
// #740 — mk(): build an element via DOM API only. textContent / setAttribute are
|
||||||
|
// NOT Trusted Types sinks (unlike innerHTML), so the banner renders on EVERY site
|
||||||
|
// — incl. strict-CSP/Trusted-Types ones (franceinfo, leparisien, 20minutes, cnn,
|
||||||
|
// x/twitter) — with ZERO CSP/TT bypass. This is the robust, permanent fix.
|
||||||
|
function mk(tag, opts){
|
||||||
|
var e = document.createElement(tag);
|
||||||
|
opts = opts || {};
|
||||||
|
if (opts.id) e.id = opts.id;
|
||||||
|
if (opts.cls) e.className = opts.cls;
|
||||||
|
if (opts.title) e.title = opts.title;
|
||||||
|
if (opts.text != null) e.textContent = opts.text;
|
||||||
|
if (opts.style) e.setAttribute("style", opts.style);
|
||||||
|
if (opts.attrs) for (var k in opts.attrs) if (Object.prototype.hasOwnProperty.call(opts.attrs, k)) e.setAttribute(k, opts.attrs[k]);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
var BTN = "border-radius:3px;padding:0 5px;margin:0 2px;font:inherit;font-size:11px;cursor:pointer";
|
||||||
function lvlSwitch(b){
|
function lvlSwitch(b){
|
||||||
var cur = String(b.level || "r1").toLowerCase();
|
var cur = String(b.level || "r1").toLowerCase();
|
||||||
// #736 — R4 = analyst / reverse-catcher tier (deepest): everything is MITM'd
|
// #736 — R4 = analyst / reverse-catcher tier (deepest): everything MITM'd +
|
||||||
// and media URLs are caught for cloning. Selectable here; functionally the
|
// media URLs caught for cloning. Reconciled into the #740 DOM-API switch.
|
||||||
// box already runs MITM-everything by default.
|
var lv = ["r0","r1","r2","r3","r4"];
|
||||||
var lv = ["r0","r1","r2","r3","r4"], out = "<span id=\"sbx-lvl\" title=\"Niveau d'analyse — R4 = analyste/capteur média, clique pour changer\">";
|
var span = mk("span", {id:"sbx-lvl", title:"Niveau d'analyse — R4 = analyste/capteur média, clique pour changer"});
|
||||||
for (var i=0;i<lv.length;i++){ var on = lv[i]===cur;
|
for (var i=0;i<lv.length;i++){ var on = lv[i]===cur;
|
||||||
out += "<button data-lvl=\"" + lv[i] + "\" class=\"sbx-lvl\" style=\"background:"
|
span.appendChild(mk("button", {cls:"sbx-lvl", text:lv[i].toUpperCase(), attrs:{"data-lvl":lv[i]},
|
||||||
+ (on?"#148C66":"transparent") + ";color:" + (on?"#0A0E14":"#8A9AA8")
|
style:"background:"+(on?"#148C66":"transparent")+";color:"+(on?"#0A0E14":"#8A9AA8")
|
||||||
+ ";border:1px solid #148C66;border-radius:3px;padding:0 5px;margin:0 1px;"
|
+";border:1px solid #148C66;"+BTN}));
|
||||||
+ "font:inherit;font-size:11px;cursor:pointer\">" + lv[i].toUpperCase() + "</button>";
|
|
||||||
}
|
}
|
||||||
return out + "</span>";
|
return span;
|
||||||
}
|
}
|
||||||
function wireLevels(bar, b){
|
function wireLevels(bar, b){
|
||||||
var els = bar.querySelectorAll(".sbx-lvl");
|
var els = bar.querySelectorAll(".sbx-lvl");
|
||||||
|
|
@ -210,29 +248,46 @@ _BANNER_CORE = r"""
|
||||||
var ck = countCookies();
|
var ck = countCookies();
|
||||||
var bar = document.createElement("div");
|
var bar = document.createElement("div");
|
||||||
bar.id = "sbx-banner";
|
bar.id = "sbx-banner";
|
||||||
bar.setAttribute("style", "position:fixed;left:0;right:0;top:0;z-index:2147483647;"
|
// #740 — !important on the visibility-critical props makes the banner IMMUNE
|
||||||
|
// to any stylesheet cosmetic hide (inline !important outranks author
|
||||||
|
// stylesheet !important): it stays ABOVE everything and visible, always.
|
||||||
|
bar.setAttribute("style", "position:fixed!important;left:0;right:0;top:0;z-index:2147483647!important;"
|
||||||
|
+ "display:flex!important;visibility:visible!important;opacity:1!important;"
|
||||||
+ "font:12px/1.4 system-ui,-apple-system,sans-serif;background:#0A0E14;color:#E8E6E0;"
|
+ "font:12px/1.4 system-ui,-apple-system,sans-serif;background:#0A0E14;color:#E8E6E0;"
|
||||||
+ "border-bottom:2px solid #148C66;padding:6px 12px;display:flex;gap:14px;align-items:center;"
|
+ "border-bottom:2px solid #148C66;padding:6px 12px;gap:14px;align-items:center;"
|
||||||
+ "box-shadow:0 2px 12px rgba(0,0,0,.4)");
|
+ "box-shadow:0 2px 12px rgba(0,0,0,.4)");
|
||||||
var pin = b.pin ? "<span title=\"pinned\">📌 " + esc(b.pin) + "</span>" : "";
|
// #740 — built entirely with DOM API (no innerHTML → not a Trusted Types
|
||||||
// #662 — 🔓 proof: the engine relaxed this page's CSP to inject this banner.
|
// sink), so the banner renders identically on every site, strict-CSP/TT
|
||||||
var cspProof = (csp === "1")
|
// included, without touching the page's CSP.
|
||||||
? "<span title=\"CSP contourné par SecuBox (démonstration)\">🔓</span>" : "";
|
bar.appendChild(mk("b", {text:"SecuBox", style:"color:#148C66"}));
|
||||||
// #683 — 🧅 kbin Tor mode: this session's exit is anonymised via Tor.
|
if (csp === "1") bar.appendChild(mk("span", {text:"🔓", title:"CSP contourné par SecuBox (démonstration)"}));
|
||||||
var tor = b.tor_mode
|
bar.appendChild(mk("button", {id:"sbx-tor", text:"🧅 " + (b.tor_mode?"ON":"OFF"),
|
||||||
? "<span title=\"Sortie anonymisée via Tor\" style=\"color:#9E76FF;font-weight:bold\">🧅 Tor</span>" : "";
|
title:"Tor du tunnel toolbox — clique pour basculer",
|
||||||
bar.innerHTML = "<b style=\"color:#148C66\">SecuBox</b>"
|
style:"background:"+(b.tor_mode?"#3D2A6B":"transparent")+";color:"+(b.tor_mode?"#C9B8FF":"#8A9AA8")+";border:1px solid #6E40C9;"+BTN}));
|
||||||
+ cspProof
|
bar.appendChild(lvlSwitch(b));
|
||||||
+ tor
|
bar.appendChild(mk("button", {id:"sbx-adg", text:"🛡️ " + (b.ad_guard===false?"OFF":"ON"),
|
||||||
+ lvlSwitch(b)
|
title:"Ad-Guard (blocage pub) — clique pour basculer",
|
||||||
+ "<span id=\"sbx-trk\">🛰️ " + trk + " trackers</span>"
|
style:"background:"+(b.ad_guard===false?"transparent":"#148C66")+";color:"+(b.ad_guard===false?"#8A9AA8":"#0A0E14")+";border:1px solid #148C66;"+BTN}));
|
||||||
+ "<span id=\"sbx-ck\">🍪 " + ck + " cookies</span>"
|
bar.appendChild(mk("span", {id:"sbx-trk", text:"🛰️ " + trk + " trackers"}));
|
||||||
+ pin
|
bar.appendChild(mk("span", {id:"sbx-ck", text:"🍪 " + ck + " cookies"}));
|
||||||
+ "<a href=\"" + esc(b.report_url || "#") + "\" style=\"margin-left:auto;color:#2C70C0;text-decoration:none\">report ▸</a>"
|
if (b.pin) bar.appendChild(mk("span", {text:"📌 " + b.pin, title:"pinned"}));
|
||||||
+ "<button aria-label=\"dismiss\" style=\"background:none;border:0;color:#8A9AA8;cursor:pointer;font-size:14px\">✕</button>";
|
bar.appendChild(mk("a", {text:"report ▸", style:"margin-left:auto;color:#2C70C0;text-decoration:none", attrs:{href: b.report_url || "#"}}));
|
||||||
|
bar.appendChild(mk("button", {text:"✕", title:"dismiss",
|
||||||
|
style:"background:none;border:0;color:#8A9AA8;cursor:pointer;font-size:14px", attrs:{"aria-label":"dismiss"}}));
|
||||||
document.body.appendChild(bar);
|
document.body.appendChild(bar);
|
||||||
try { document.body.style.paddingTop = (bar.offsetHeight || 34) + "px"; } catch (_) {}
|
try { document.body.style.paddingTop = (bar.offsetHeight || 34) + "px"; } catch (_) {}
|
||||||
wireLevels(bar, b);
|
wireLevels(bar, b);
|
||||||
|
// #740 — 🛡️ Ad-Guard + 🧅 Tor quick-toggles (mirror the level switch wiring).
|
||||||
|
var adg = bar.querySelector("#sbx-adg");
|
||||||
|
if (adg) adg.onclick = function(){ var on = b.ad_guard!==false; adg.textContent="…";
|
||||||
|
fetch("/__toolbox/set-adguard?on=" + (on?"0":"1"), {credentials:"omit",cache:"no-store"})
|
||||||
|
.then(function(r){ if(r&&r.ok) location.reload(); else adg.textContent="🛡️ "+(on?"ON":"OFF"); })
|
||||||
|
.catch(function(){ adg.textContent="🛡️ "+(on?"ON":"OFF"); }); };
|
||||||
|
var tg = bar.querySelector("#sbx-tor");
|
||||||
|
if (tg) tg.onclick = function(){ var on = !!b.tor_mode; tg.textContent="…";
|
||||||
|
fetch("/__toolbox/set-tor?on=" + (on?"0":"1"), {credentials:"omit",cache:"no-store"})
|
||||||
|
.then(function(r){ if(r&&r.ok) location.reload(); else tg.textContent="🧅 "+(on?"ON":"OFF"); })
|
||||||
|
.catch(function(){ tg.textContent="🧅 "+(on?"ON":"OFF"); }); };
|
||||||
var btn = bar.querySelector("button[aria-label=\"dismiss\"]");
|
var btn = bar.querySelector("button[aria-label=\"dismiss\"]");
|
||||||
if (btn) btn.onclick = function(){ dismissed = true; try { document.body.style.paddingTop = ""; } catch (_) {} bar.remove(); };
|
if (btn) btn.onclick = function(){ dismissed = true; try { document.body.style.paddingTop = ""; } catch (_) {} bar.remove(); };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,13 @@ def test_inline_csp_literal_and_proof_logic():
|
||||||
|
|
||||||
def test_inline_has_no_currentscript_no_fetch():
|
def test_inline_has_no_currentscript_no_fetch():
|
||||||
# #653 root cause: document.currentScript is null in an async context. The
|
# #653 root cause: document.currentScript is null in an async context. The
|
||||||
# inline script MUST NOT read it, and MUST NOT fetch() (SW would hijack it).
|
# inline script MUST NOT read it, and MUST NOT fetch the BUNDLE at load time
|
||||||
|
# (a SW would hijack a same-origin /__toolbox/bundle fetch → no banner). The
|
||||||
|
# bundle is baked inline instead. (#740 toggle handlers DO fetch /set-level
|
||||||
|
# etc., but only on a user click — not a load-time SW-hijackable resource.)
|
||||||
s = bundle.inline_script("x", wg=True, csp=True)
|
s = bundle.inline_script("x", wg=True, csp=True)
|
||||||
assert "currentScript" not in s
|
assert "currentScript" not in s
|
||||||
assert "fetch(" not in s
|
assert "/__toolbox/bundle" not in s
|
||||||
|
|
||||||
|
|
||||||
def test_inline_keeps_guards_and_spa_hooks():
|
def test_inline_keeps_guards_and_spa_hooks():
|
||||||
|
|
@ -133,6 +136,6 @@ def test_inline_route_returns_javascript_body():
|
||||||
body = resp.body.decode("utf-8")
|
body = resp.body.decode("utf-8")
|
||||||
assert "window.__SBX_LOADER__" in body
|
assert "window.__SBX_LOADER__" in body
|
||||||
assert "currentScript" not in body
|
assert "currentScript" not in body
|
||||||
assert "fetch(" not in body
|
assert "/__toolbox/bundle" not in body # bundle baked inline, not fetched (#740 toggles fetch /set-* on click only)
|
||||||
assert 'var mh = "abc";' in body
|
assert 'var mh = "abc";' in body
|
||||||
assert 'var csp = "1";' in body
|
assert 'var csp = "1";' in body
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user