mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-26 19:31:43 +00:00
e4a21fc9ab
## Summary Closes the "XSS regression in newly-added sink" class. Follow-up to #1537 (10 stored-XSS sinks in node names) and the post-#1537 audit (TRACE-1, OBS-1, ANL-1 — 3 additional HIGH XSS in files #1537 didn't touch). After those fixes land, the project still has **zero automated catch for the next one**. Every future PR can re-introduce the same class freely. This PR closes that gap with a hard-fail pr-preflight gate that runs at PR-creation time and in CI. ## What the gate does A NEW or MODIFIED line in the PR diff under `public/**/*.{js,html}` is flagged when it matches any of these sink patterns: | Pattern | What it catches | |---|---| | `.innerHTML = \`…\`` / `'…'` | template-literal or string-concat HTML injection | | `insertAdjacentHTML(…, \`…\`)` | DOM-adjacent injection | | `.bindPopup(\`…\`)` / `.bindTooltip(\`…\`)` | Leaflet popup/tooltip injection (the OBS-1 class) | | `.setAttribute('on<event>', …)` | inline event-handler injection | | `.setAttribute('href'\|'src'\|'action'\|'formaction', <interp>)` | `javascript:` URI class | For each flagged line, the gate then walks the dynamic substring (`${…}`, post-`+`, or `setAttribute` value arg) and only fires if it interpolates an identifier from the node-controlled allowlist (`name`, `observer`, `sender`, `pubkey`, `body`, `hash`, …). This keeps the regex off static CSS classes like `text-center`. A flagged line is accepted (no fail) when ANY of: - **(a)** wrapped in `escapeHtml(` / `escapeAttr(` / `safeEsc(` / local `esc(` — the audited helpers - **(b)** a same-PR `test*.js` file DOM-greps the audit payload (`' onfocus=` or `onerror=alert`) AND references the sink file's basename - **(c)** the PR body carries `PREFLIGHT-XSS-OPTOUT: <file>:<line> reason="…"` — explicit author opt-out logged for reviewer attention Otherwise: **HARD FAIL** with `file:line: flagged: <token>` plus a suggested fix. ## Split - **Skill directory** (local, no PR): - `~/.openclaw/skills/pr-preflight/scripts/check-xss-sinks.sh` — canonical gate - `~/.openclaw/skills/pr-preflight/data/xss-node-controlled-fields.txt` — allowlist (27 identifiers, easy to extend without a repo PR) - wired into `~/.openclaw/skills/pr-preflight/scripts/run-all.sh` - **This PR** (in repo): - `testdata/preflight-xss/` — fixtures (`bad-1..bad-3`, `good-1..good-2`, `test-good-2.js`) - `scripts/check-xss-sinks.sh` — local mirror of the canonical gate, so CI can exercise the gate without depending on the skill dir - `test-preflight-xss-gate.js` — Node test wrapper that asserts bad fixtures fail (exit 1) and good fixtures pass (exit 0) - `public/app.js` — `escapeHtml` docstring marked CANONICAL with links to the enforcing gate - `.github/workflows/deploy.yml` — invoke `node test-preflight-xss-gate.js` alongside the existing `test-xss-escape-sinks.js` ## TDD red → green | | Commit | Test result | |---|---|---| | **Red** | `test(preflight-xss): RED — fixtures + assertion wrapper for XSS sink gate` | `test-preflight-xss-gate.js` exits 1 — bad fixtures unexpectedly pass because `scripts/check-xss-sinks.sh` is a no-op stub. Genuine assertion failure (not a build error). | | **Green** | `feat(preflight): GREEN — implement XSS-sink check + escapeHtml docstring` | stub replaced with real check; all 5 fixtures behave as expected. | The red commit ships a working stub script so the test runs to completion and fails on an **assertion**, not on a missing-file error. ## Coverage proof — would the gate have caught the originals? - **PR #1537 (10 sinks):** synthetic file from the deleted lines of #1537 → gate flags `n.name` in `innerHTML \`tpl\`` and two `bindPopup(\`…${n.name}\`)` lines. Yes, the gate would have caught these the moment they hit a PR diff. - **Post-#1537 audit:** - **TRACE-1** (`traces.js` `${e.message}` / `${urlHash}` in innerHTML): yes — the `hash`/`urlHash` tokens are allowlisted and the innerHTML template-literal pattern matches. - **OBS-1** (`observer-detail.js` URL fragment + MQTT fields into innerHTML / bindPopup): yes — the `observer`, `text`, `hash` tokens are allowlisted and both sink patterns match. - **ANL-1** (`analytics.js` attribute-mutation roundtrip): yes for `setAttribute('on*', …)` and `setAttribute('href', \`…${interp}…\`)` patterns. (Note: pure innerHTML lines with only `${e.message}` are not node-controlled and are intentionally not flagged.) ## Allowlist (initial 27 identifiers) ``` adv_name name observer observer_name sender from_node channel channel_name model firmware client_version radio iata hopNames nodeLabel obsName n.name o.name obs.name public_key pubkey area_key region_name text body message preview hash urlHash ``` Extend in `~/.openclaw/skills/pr-preflight/data/xss-node-controlled-fields.txt` whenever a new node-controlled field surfaces in an audit — no repo PR required. ## Hard rules respected - No build step, no ESLint plugin, no AST analysis — grep + heuristics + opt-out escape valves - Hard fail (exit 1), not warning-only (exit 2) - PII preflight grep on every commit + this PR body - Same split as the sibling migration-gate PR ## Three-axis merge-readiness - **Mergeable:** yes — branch is clean off `origin/master`, no conflicts - **CI:** will report on push; red commit expected to fail, green commit expected to pass - **Threads:** none open yet (new PR) --------- Co-authored-by: meshcore-bot <bot@local> Co-authored-by: mc-bot <bot@meshcore.local> Co-authored-by: corescope-bot <bot@corescope>