Skip to content

Add a CI guard for report injection and remote destinations - #394

Merged
abrignoni merged 1 commit into
mainfrom
feat/html-safety-ci
Aug 7, 2026
Merged

Add a CI guard for report injection and remote destinations#394
abrignoni merged 1 commit into
mainfrom
feat/html-safety-ci

Conversation

@abrignoni

Copy link
Copy Markdown
Owner

Port of iLEAPP #1891, with this core's own baseline.

Why

html_columns cells are written to the report without html.escape, which makes them an injection sink, and any remote href/src makes opening a report a disclosure event. GHSA-45q2-q93c-cfv2 fixed the escaping by hand in July and nothing has watched it since.

The drift is not hypothetical. The same waze format_url() was fixed in iLEAPP and left anchoring evidence URLs in ALEAPP until a corrected run of this very check found it (ALEAPP #1040).

What it checks

admin/scripts/check_html_safety.py, wired into python_lint.yml beside check_claim_language.py. Three AST rules:

  • unescaped-interpolation — markup-bearing f-strings, + concatenation, %, .format() and "<br>".join() must escape what they interpolate.
  • remote-destination — no literal remote URL in markup, and no href/src completed by an expression that is not report-relative.
  • unguarded-html-columns — a module declaring html_columns must call an escaper somewhere.

Scoped to modules that can actually reach a no-escape cell: a declared html_columns, a media column, or a named framework helper. Elsewhere the writer escapes the whole cell, so hand-built markup is a display bug, not an injection.

Baseline

BASELINE carries what this core has today so the gate can go up without blocking anyone; new findings fail. Both lists also fail the run when an entry stops matching, so the debt list can only shrink. Entries are keyed by (path, rule, function) and survive line moves.

Entry What it is
instagramMessages, instagramMessageReq Real. Both build <table>/<tr> accumulators out of evidence values without escaping.
kikReturns Safe in fact — it appends esc(line) — but the accumulator has mixed assignments this check cannot order.
semanticLocbymonth ×2 Joins waypoint coordinates into an html_columns cell with no escaping. Numeric in practice, unenforced.
media_to_html ×2 Analysis limit, as above.

Validation

  • pylint --disable=C,R = 10.00; workflow YAML parses
  • exits 0 on this tree as it stands
  • exits 1 on injected unescaped-interpolation and remote-src probes, and returns to 0 when they are reverted

🤖 Generated with Claude Code

Port of iLEAPP #1891, with this core's own baseline.

html_columns cells are written to the report without html.escape, which makes
them an injection sink, and any remote href/src makes opening a report a
disclosure event. The escaping was fixed by hand in GHSA-45q2-q93c-cfv2 and
nothing has watched it since. The drift is not hypothetical: the same waze
format_url() was fixed in iLEAPP and left anchoring evidence URLs in ALEAPP
until a corrected run of this very check found it.

admin/scripts/check_html_safety.py, wired into python_lint.yml beside
check_claim_language.py. Three AST rules -- unescaped-interpolation over
markup-bearing f-strings, + concatenation, %, .format() and "<br>".join();
remote-destination over literal remote URLs and any href/src not provably
report-relative; and unguarded-html-columns for a module that declares
html_columns and calls no escaper.

Scoped to modules that can reach a no-escape cell: a declared html_columns, a
media column, or a named framework helper. Elsewhere the writer escapes the
whole cell, so hand-built markup is a display bug rather than an injection.

BASELINE carries what this core has today so the gate can go up without
blocking anyone; new findings fail. Both lists also fail the run when an entry
stops matching, so the debt list can only shrink. Entries are keyed by
(path, rule, function) and survive line moves. Every entry says what it is and
what would clear it.

Validated: pylint --disable=C,R = 10.00; workflow YAML parses; the check exits
0 on this tree and exits 1 on injected unescaped-interpolation and
remote-src probes, returning to 0 when they are reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abrignoni
abrignoni merged commit 4f4a285 into main Aug 7, 2026
1 check passed
@abrignoni
abrignoni deleted the feat/html-safety-ci branch August 7, 2026 02:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant