| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
…2-E1)
Regression reports (a Markdown writeup + SVG residual plot + JSON fact-box per
fit) are dataset-authored content served to anonymous users, but the build copied
the SVG/JSON verbatim and rendered the Markdown without sanitization. S2-E1 adds
the security boundary and the dataset-validation checks for that content, without
yet moving the files to the dataset (that is S2-E2/E3) — so it is backward-
compatible and hardens the existing docs/regression path in place.
New `src/kayak/web/regression.py` (used by both build and validate-dataset):
- render_markdown_safe: filter maintainer sections → python-markdown → nh3.clean
with an explicit tag/attr allowlist, http(s)-only URL schemes, link rel, and a
local-only img@src filter (strips raw HTML, event handlers, javascript:/data:,
external image sources).
- validate_svg: defusedxml parse (no DTD/entities/external) + a strict
element/attribute allowlist, then re-serialize from the validated tree (never
serve verbatim). Rejects script/foreignObject/use/image/event-handlers/href/
style, foreign-namespace elements, backslash CSS-escapes, and any resource
function (url()/image()/element(), case-insensitive) that isn't a same-document
url(#id). SVG is treated as active content (the browser renders /static/.../
<slug>.svg as a document outside the page CSP).
- validate_json_sidecar: size cap + reject NaN/Infinity + object-with-slug shape
(accepts both the pair-linear and lead/lag schemas).
Build (web/build/deploy.py): _deploy_regression_artifacts now renders/validates
through the module (re-serialized SVG, no shutil.copy2) and is fail-closed.
validate-dataset: new _check_regression ties every non-empty
calc_expression.provenance_slug to its {md,svg,json} triple, follows md links to
require companion reports + referenced sidecars (order-independent; code-fence-
aware), runs the content through the sanitizers (reject nonconforming), and warns
on orphan reports via a non-fatal warnings channel that leaves validate_dataset()'s
list[str] error contract intact. No slugs + no regression/ dir = none configured.
Deps: add nh3 + defusedxml (regenerated uv.lock; mypy override for defusedxml).
Fixture: a declared report + a link-reachable lead/lag companion under
tests/fixtures/dataset/regression/ (authored by build_dataset_fixture.py so a
regen reproduces it), exercising the slug↔report check, the maintainer-section
drop, the reference closure, and the SVG/JSON sanitizers. All 25 real reports
pass the new checks (move-verbatim invariant). Two rounds of adversarial review
(incl. headless-browser bypass testing) drove the SVG allowlist hardening.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review — PR #152Findings1. [HIGH] This will fail production deploy against the current kayak_data checkout. deploy.sh unconditionally runs levels validate-dataset "$KAYAK_DATA" before any metadata sync (scripts/deploy.sh:145-159). This PR adds _check_regression() to that validator and makes any non-empty calc_expression.provenance_slug require a <dataset>/regression/ directory with the matching {md,svg,json} triple (src/kayak/cli/validate_dataset.py:170-175, :1128-1136). But the PR explicitly says S2-E1 has not moved the reports into kayak_data yet, and the current adjacent kayak_data checkout has no regression/ directory while calc_expression.csv declares 17 provenance slugs. I reproduced the deploy gate directly from this PR head: $ levels validate-dataset /Users/pat/tpw/kayak_data
dataset validation FAILED (/Users/pat/tpw/kayak_data):
- regression/ directory missing but provenance_slug(s) declared: [...]
So merging this code alone bricks the next deploy before migrate/sync/build. Either gate the new <dataset>/regression validation until S2-E2/E3 actually lands the files in kayak_data, or make this PR dependent on the matching kayak_data PR and deploy them together. The current "backward-compatible and hardens existing docs/regression path in place" claim does not hold with the unconditional deploy validator. 2. [MED] The PR reverts current main operator docs for SA-teardown-C. The diff against main deletes the deploy/SETUP.md text that tells the operator to pause/remove the retired metadata-snapshot check on healthchecks.io, and weakens the kayak_data branch-protection step from "require PR + validate check + enforce_admins" to just "enable branch protection" (deploy/SETUP.md:471-478). This looks like a stale branch carrying an older copy of the PR #150 follow-up note. If merged as-is, it removes the false-alert cleanup and the concrete branch-protection settings from the canonical deploy doc. Verification
|
Sorry, something went wrong.
Adversarial review — PR #152 (S2-E1: sanitize + validate regression content)Reviewed on the live host. This is a security PR whose trust boundary is the SVG served as a standalone document outside the page CSP — a stored-XSS surface — so I built an isolated venv with the PR's pinned deps (nh3 0.3.5, defusedxml 0.7.1, markdown) and ran a bypass battery against the actual regression.py, plus verified the build path and false-positive rate on the 25 real reports. Verdict: strong, merge-worthy security work. The SVG/markdown sanitizers held up to every attack I threw; the architecture (allowlist + re-serialize-from-tree, nh3 backstop, fail-closed build) is right. Two defense-in-depth findings below — one a genuine (if low-severity, orphan-only) XSS path that this exact PR is the right place to close, one a robustness note. Neither is a blocker. What I verifiedSVG bypass battery — all defeated (each rejected, or stripped by re-serialization with the clean bytes served):
Markdown — nh3 strips everything dangerous: raw <script>, <img onerror>, [x](javascript:…), entity-obfuscated javascript:, external/data:/protocol-relative img src, <iframe>, raw <svg onload>, autolinked javascript: → all gone; links get rel="noopener noreferrer nofollow"; img src is local-only. Build is fail-closed (_deploy_regression_artifacts re-raises UnsafeContentError → build aborts) and serves the re-serialized validate_svg() return (deploy.py:172-175), never the verbatim file. Zero false-positives: all 25 real SVGs + 25 JSONs pass and all 26 MDs render — the move-verbatim invariant holds. Findings1. [Low–Med, defense-in-depth] Unescaped report filename injected into the generated <title> — an orphan-file XSS path. In _deploy_regression_artifacts the page is built with title = path.stem.replace("_"," ") interpolated into <title>{title} — Regression analysis</title> without HTML-escaping (deploy.py ~:191). A regression file whose name contains HTML metacharacters injects: I confirmed evil</title><script>alert(1)</script>.md yields a <title>…</title><script>alert(1)</script>… page.
2. [Low, defense-in-depth] validate_svg accepts-and-strips processing instructions rather than rejecting them. A <?xml-stylesheet type="text/css" href="javascript:…"?> is accepted (no UnsafeContentError). I confirmed it is not exploitable today: ElementTree drops PIs on parse, so the re-serialized output is a clean <svg><rect/></svg>, and the build serves that re-serialized output. But the safety rests on that ET implementation detail — a future swap to a PI-preserving serializer (lxml, or insert_pis) would leak it — and as a validator, validate-dataset silently passes an SVG carrying a dangerous PI rather than flagging it. Consider rejecting PIs/comments explicitly so the "reject nonconforming" contract doesn't depend on the serializer's drop behavior. Nits / notes
Net: the core sanitizers are genuinely solid and I couldn't break them; closing the <title> escape (and tightening the build's file set to validated slugs) would make E2/E3 land clean. |
Sorry, something went wrong.
…G PI/comment 1. [HIGH] Deploy-brick: deploy.sh runs `validate-dataset` against the real kayak_data on every deploy, and the new _check_regression made the 17 declared provenance_slugs require a regression/ directory that won't exist until the S2 file-move (D1). Deploying E1 alone would have failed the gate before migrate/sync. Fix: the regression/ directory is the dataset's opt-in — while it is absent the reports still live engine-side (docs/regression), so declared slugs yield a non-fatal WARNING, not an error. Once the dir exists the check is fully enforced (the fixture exercises that path). Real dataset now validates OK (1 warning); fixture still fully enforced. 2. [Low-Med] Orphan-filename XSS: the generated report page interpolated the file stem into <title> unescaped, and the build globbed every *.md regardless of slug — so an orphan file whose NAME carried HTML metacharacters could emit a stored-XSS page (validate-dataset only warns on orphans). Fix: the build now skips any regression file whose stem isn't a safe slug (new kayak.web.regression.is_safe_slug, shared with the validator's charset), and html.escape()s the title as defence-in-depth. (Also renamed the shadowing local `html` template var → `page`, and added encoding= to the page write.) 3. [Low] validate_svg now rejects XML comments and processing instructions explicitly (e.g. `<?xml-stylesheet href="javascript:…"?>`), so the reject-nonconforming contract no longer rests on ElementTree silently dropping them. A single leading `<?xml …?>` declaration is still allowed. Also merged current main to pick up #151's deploy/SETUP.md (the branch predated it, so the diff had appeared to revert the healthchecks.io SA-teardown-C note). All 25 real reports still pass; full gate green (1536 tests, wheel-smoke). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed in 5495b50 (plus a merge of current main). Thanks — the deploy-gate brick was a real ordering bug I'd missed. Finding 1 (HIGH) — deploy-brick. You're right: deploy.sh runs validate-dataset against the real kayak_data on every deploy, so making the 17 declared slugs require a regression/ dir that doesn't exist until D1 would have failed the gate. Fix: the regression/ directory is the dataset's opt-in — while it's absent the reports still live engine-side (docs/regression, until the S2 file-move), so declared slugs now yield a non-fatal WARNING, not an error. Once the dir exists the check is fully enforced (the fixture covers that path). Verified: $ levels validate-dataset /Users/pat/tpw/kayak_data ! 17 provenance_slug(s) declared but no regression/ directory (reports not yet managed by this dataset) dataset validation OK (/Users/pat/tpw/kayak_data) (1 warning(s)) So E1 is now genuinely deploy-safe on its own; D1 lands the files and the check engages. Finding 2 (MED) — reverted SETUP.md. Stale-branch artifact: this branch predated #151's merge, so the diff appeared to delete the healthchecks.io / branch-protection note. Merged current main — the #151 content is back (E1 doesn't touch SETUP.md, clean merge). Security review finding 1 (orphan-filename XSS). Fixed both ways you suggested: the build now skips any regression file whose stem isn't a safe slug (new kayak.web.regression.is_safe_slug, shared charset with the validator), and html.escape()s the <title> as defence-in-depth. A bad<x>.{md,svg,json} is no longer processed (new test). (Also fixed a html-module shadowing the local template var, and added encoding= to the page write — the nit you flagged.) Security review finding 2 (SVG PI/comment). validate_svg now rejects XML comments and processing instructions explicitly (e.g. <?xml-stylesheet href="javascript:…"?>), so the reject-nonconforming contract no longer rests on ElementTree dropping them. A single leading <?xml …?> declaration is still allowed. (New tests.) All 25 real reports still pass; full gate green locally (1536 tests, wheel-smoke, ruff/mypy). Holding for your go before merge. |
Sorry, something went wrong.
Adversarial rereview — PR #152 follow-up (5495b50)No blocking findings. I rereviewed the follow-up commit and re-ran the deploy-sensitive checks against the current adjacent kayak_data checkout and the fresh live DB copy at /Users/pat/tpw/DB/kayak.db. Prior findings are addressed:
Verification:
Residual note: until the S2 file-move lands, regression content is still validated at build time from code-repo docs/regression/, while kayak_data gets the transitional warning. That matches this PR's stated E1/D1 split. |
Sorry, something went wrong.
Adversarial re-review — PR #152 (after 5495b50)Re-reviewed the fix commit on the live host: rebuilt an isolated venv with the PR deps, re-ran the bypass battery against the updated regression.py, and verified the deploy-brick scenario against the real kayak_data. (CI still running per your note; this review is on the code + my own testing, not a CI result.) Verdict: the fixes are solid — both my round-1 findings are resolved, and the commit caught a HIGH I missed. One residual low gap in the PI fix (below), not a blocker. Resolved
The HIGH I missed — credit to the fixdeploy.sh runs validate-dataset against the real kayak_data on every deploy, and the new _check_regression made the 17 declared provenance_slugs require a regression/ directory that won't exist until the D1 file-move. I verified the trigger on the live host: kayak_data has 17 non-empty provenance_slugs (of 22 rows) and no regression/ dir — so the pre-fix code would have errored and bricked the E1 deploy at the validate-dataset gate (the same gate-brick class as #149/#150, which I also under-checked). The fix correctly treats an absent regression/ dir as the transitional opt-out (declared slugs → non-fatal warning), with full enforcement once the dir exists (the fixture still exercises the enforced path). Real dataset now validates clean (1 warning). This was the right call and I should have caught it in round 1. Residual finding[Low, defense-in-depth] Fix #3 still doesn't reject a leading <?xml-stylesheet …?> PI. The check strips a leading <?xml …?> as the XML declaration via body.startswith("<?xml") — but that prefix also matches <?xml-stylesheet, so a leading <?xml-stylesheet type="text/xsl" href="…"?> (or href="javascript:…") is treated as the declaration, stripped, and not flagged — the exact construct the commit message cites as what it's fixing. I verified both the js and xsl variants are accepted (while the in-body PI, leading/in-body comments, and <?foo?> leading PI are all correctly rejected, and the legit <?xml version?> declaration still passes). Not exploitable — I confirmed the PI is absent from the re-serialized output (leak_in_output=False) and the build serves only that re-serialized SVG — so this is purely the "reject-nonconforming contract" not yet covering its own example. One-line fix: distinguish the declaration by the whitespace the XML spec requires after the xml target, e.g. re.match(r"<\?xml\s", body) instead of startswith("<?xml") — the PI target xml-stylesheet has a -, not whitespace, so it then falls through to the "<?" in body reject. Re-verified, no regressionsThe round-1 battery still holds against the updated code: script/onload/uppercase-URL()/foreign-namespace/xlink:href all still rejected; all 25 real reports still pass (move-verbatim invariant intact). nh3 markdown sanitization unchanged. Net: ship-worthy once the leading-PI prefix is tightened (or explicitly accepted as known/again-defense-in-depth). Nice work closing the deploy-brick — that was the load-bearing one. |
Sorry, something went wrong.
…review)
The PI/comment guard treated any leading `<?xml…` as the XML declaration via
`startswith("<?xml")`, which also matched a leading `<?xml-stylesheet …?>` PI —
so that one construct (the very example the guard cites) was stripped, not
flagged. Distinguish the real declaration by the whitespace the XML spec requires
after the `xml` target (`re.match(r"<\?xml\s", body)`); the PI target
`xml-stylesheet` has a `-`, so it falls through to the reject. Not exploitable
(ElementTree drops the PI and the build serves the re-serialized SVG), but it
closes the reject-nonconforming contract over its own example.
Tests: leading xml-stylesheet PI now rejected; a genuine `<?xml version?>`
declaration still accepted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the residual in 786a9d9. Good catch — the guard didn't cover its own example. Leading <?xml-stylesheet …?> PI. The check distinguished the XML declaration with startswith("<?xml"), which also matched the xml-stylesheet PI target, so a leading stylesheet PI was stripped as if it were the declaration. Switched to re.match(r"<\?xml\s", body) — the declaration requires whitespace after the xml target, while xml-stylesheet has a -, so it now falls through to the reject. Verified:
(Not exploitable — ElementTree drops the PI and the build serves the re-serialized SVG — but it closes the contract over its own example.) Full gate green locally (1538 tests, wheel-smoke, ruff/mypy). All 25 real reports still pass; deploy gate on the real kayak_data still OK (1 transitional warning). Holding for your go before merge. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
First PR of slice S2 (regression content → dataset). The full S2 arc moves the published regression reports out of the engine repo into kayak_data and hardens how they're served; this PR (E1) adds the sanitization + validation capability and the dataset-validation checks without moving any files yet — so it's backward-compatible and immediately hardens the existing docs/regression build path. It becomes the trusted engine_test_ref for the later dataset PR.
Decomposition (each its own PR): E1 (this) → D1 (kayak_data adds regression/) → D2 (pin bump) → E2 (build/generator source → DATASET_DIR) → E3 (delete engine copy). Ordering keeps reports served throughout.
What's in E1
New src/kayak/web/regression.py — pure sanitizers used by both levels build and levels validate-dataset:
Build (web/build/deploy.py): _deploy_regression_artifacts now renders/validates through the module (re-serialized SVG, no shutil.copy2) and is fail-closed.
validate-dataset: new _check_regression ties every non-empty calc_expression.provenance_slug to its {md,svg,json} triple, follows md links to require companion (lead/lag) reports + referenced sidecars (order-independent + code-fence-aware), runs content through the sanitizers (reject nonconforming), and warns on orphan reports via a non-fatal warnings channel that leaves validate_dataset() -> list[str]'s error contract intact. No slugs + no regression/ dir = "none configured".
Deps: add nh3 + defusedxml (regenerated uv.lock; mypy override for defusedxml). Fixture: a declared report + a link-reachable lead/lag companion under tests/fixtures/dataset/regression/, authored by build_dataset_fixture.py so a regen reproduces it.
Verification
Not in this PR
File move (E2/E3), the kayak_data regression/ dir + pin bump (D1/D2), and the generator --out default repoint (E2). Build still reads BASE_DIR/docs/regression here.
🤖 Generated with Claude Code