Add a CI guard for report injection and remote destinations - #394
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Port of iLEAPP #1891, with this core's own baseline.
Why
html_columnscells are written to the report withouthtml.escape, which makes them an injection sink, and any remotehref/srcmakes 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
wazeformat_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 intopython_lint.ymlbesidecheck_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 nohref/srccompleted by an expression that is not report-relative.unguarded-html-columns— a module declaringhtml_columnsmust 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
BASELINEcarries 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.instagramMessages,instagramMessageReq<table>/<tr>accumulators out of evidence values without escaping.kikReturnsesc(line)— but the accumulator has mixed assignments this check cannot order.semanticLocbymonth×2html_columnscell with no escaping. Numeric in practice, unenforced.media_to_html×2Validation
pylint --disable=C,R= 10.00; workflow YAML parsesunescaped-interpolationand remote-srcprobes, and returns to 0 when they are reverted🤖 Generated with Claude Code