fix: inline comments and disposition replies should not render raw html from claim text #20
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/escape-html-in-comments"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Claim titles and bodies, disposition rationales and fix refs now go through
neutralizeHtml, so tags in them show as text while code still shows verbatim. The module is a copy of the one in j4k/review#101. Importing it would have to wait for that PR and a release, so merging the two copies is a follow-up.Markers now count only at the start of a comment. A
<!-- review:claim:… -->quoted inside a claim body no longer takes over a thread or blocks a post.The rationale now sits below the label as a quoted block, because a fence in it split the label's paragraph. Existing replies still match by marker, so none are reposted.
After merge, repin
sourceCommitand its audit record in j4k/alignsrc/review-wrapper-plan.ts, then re-render each consumer's review workflow.Review
01M41BXJNPMEM7K4YTGYH71DHG— head41cd66132f7ce152dca1b7c1ab93c93be05eb674Review — j4k-oss/review-wrapper @
7d42be7032Scope: diff against base tree
3c6708711f1cStatus: dispatched — coverage complete (3/3 slots terminal)
Facts: current review-wide projection
Computed under:
Findings (8)
medium — A user-authored claim marker makes the projector modify a human thread
01M41CC43F61YQR67CV4RS6S2Wsrc/reconcile/conversation-markers.ts(snippet)low — 404 warning buries the reply-only fallback behind an unverified cause list
01M41C5ZFSF1GF37Y0XWEEZFEDsrc/reconcile/conversation-markers.ts(snippet)low — Supersession tests bypass the marker check with resolved anchors
01M41C4A6W41QHZB2XDGY8N38Msrc/reconcile/dispositions.test.ts(snippet)low — 404 degradation test misses repeated failed resolution attempts
01M41C76CWHMBSSAH5Q8CD7203src/reconcile/dispositions.test.ts(snippet)low — Superseded-thread comment incorrectly says labelled open threads get no write
01M41C4N2CMVTGD5ZDSBAAZDQJsrc/reconcile/dispositions.ts(snippet)low — Head-file fetch test cannot distinguish a path-anchor fetch
01M41C56MFSZ8FXPYCJWDAS059src/reconcile/inline.test.ts(snippet)low — Generated comment test ignores raw code tags from untrusted fields
01M41C69PS48WM3TGG6SQR0V4Zsrc/reconcile/inline.test.ts(snippet)low — Leading-marker comment claims authorship that the code never verifies
01M41C79EYTYDA8PDAJQFSC2ZFsrc/reconcile/inline.ts(snippet)Other claims
Coverage
Coverage pass: 01M41BXJQ48JWWTWJF0WY54A4F
Accounting: complete
Slot health: healthy
@ -0,0 +73,4 @@it("leaves code without a live `<` as markdown", () => {expect(neutralizeHtml("`a<b` and ``a ` <b``", "block")).toBe("`a<b` and ``a ` <b``");expect(neutralizeHtml("```ts\nconst a = 1;\n```", "block")).toBe("```ts\nconst a = 1;\n```");low — Fence identity example never reaches the code-without-a-live-tag path
lens
test-trimming· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3Z88X5FJMGY3P8HZST0JVNPof review01M3Z7F0GWMNWZE1D6BB856D1PFixed in
483cf33: the fence example now holds a non-tag<, so it passes the early return and parses the fence.@ -0,0 +151,5 @@});});// Each case reproduced a bypass of the hand-rolled tokenizer through Forgejo's renderer.describe("neutralizeHtml closes the reported bypasses", () => {low — Tests name a past bypass incident instead of the invariant they guard
lens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3Z7S16FGF6V295AQHZX0WVFof review01M3Z7F0GWMNWZE1D6BB856D1PFixed in
483cf33: describe, test, and fixture comments now state the invariant instead of the incident.@ -0,0 +7,5 @@* run of blocks (a quoted body). */export type MarkdownContext = "inline" | "block";// Emphasis cannot move a code or destination boundary, and micromark resolves it in// quadratic time: a 64 KiB body of `*a_` took 8.7 s with it and 15 ms without.low — Emphasis-disable comment stores one-run timings instead of the durable rule
lens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3Z7S74DRHM6CQEEN51A555Gof review01M3Z7F0GWMNWZE1D6BB856D1PFixed in
483cf33: the comment states the durable rule; timings removed here and from the cost test.@ -0,0 +46,5 @@["definitionDestinationLiteral", "destination"],]);/** Source CommonMark + GFM tables keep literal: a code span or block with the text it* displays, or an angle-bracket link destination with the text between its brackets. */low — LiteralRange JSDoc is ungrammatical so the type's meaning is unclear
lens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3Z7SCVHVKW4S416QPBTS37Cof review01M3Z7F0GWMNWZE1D6BB856D1PFixed in
483cf33: LiteralRange JSDoc rewritten as a full sentence.@ -0,0 +36,5 @@return rawHtmlNodes(markdown).filter((value) => !EMITTED.test(value));}/** Inputs whose neutralized form still holds raw HTML a parser sees, or any `<` that could* open a tag outside the HTML `neutralizeHtml` reports it emitted. */low — rawHtmlViolations JSDoc describes inputs but the function returns diagnostics
lens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3Z7T32BAEG6A7R8WM1XSBFXof review01M3Z7F0GWMNWZE1D6BB856D1PFixed in
483cf33: rawHtmlViolations JSDoc now says it returns diagnostic strings.@ -0,0 +3,8 @@import { parseLiteralRanges } from "./parse-literal-ranges.ts";/** A `<` that can open raw HTML in a CommonMark renderer: `</`, `<!`, `<?`, or a tag name* followed by whatever may continue a tag (whitespace, `/`, `>`, end of text). Every other* `<` — `a < b`, `<https://x>`, `<user@example.com>` — is text in every dialect, so* autolinks survive untouched. Names take any letter, not just ASCII: goldmark matches* `<script` case-insensitively, and Go folds `ſ` to `s`. `\s` is the Unicode class, a* superset of what any renderer counts as whitespace. */low — HTML sanitizer comment claims guarantees for every Markdown dialect
lens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZADYYX9WVKTC33RDTWW47Cof review01M3ZA5YDVGQRGVDHSD059P8MZFixed in
ebf66cc: the TAG_START comment now scopes its claims to CommonMark and Forgejo's renderer.@ -18,3 +17,3 @@claimMarker(claim.id),`**${claim.severity}** — ${claim.title}`,`**${claim.severity}** — ${neutralizeHtml(claim.title, "inline")}`,`lens \`${claim.lens}\` · arm \`${claim.arm}\` · ${tally}${wavering}`,low — Inline findings expose triage metadata without explaining its terms
lens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZACZTYPD0RTXVP0KE71NJQof review01M3ZA5YDVGQRGVDHSD059P8MZThis line is not part of this PR: the lens/arm/tally/wavering line is identical on main (src/reconcile/inline.ts:11-20); the PR only changes how the title and body are neutralized. The terms come from the external review service's contract, which the finding says it did not inspect, and the published comment format is parsed by downstream tooling, so redefining it belongs in its own change.
superseded by review
01M3ZAP3N3GZTRF3T09RWJKY6Efor headebf66cc54af2a7ca4a5434ad472adf6a5c93f80b@ -0,0 +1,21 @@import { PIECES } from "./markdown-pieces.fixture.ts";/** Deterministic random markdown built from those fragments. */low —
generateMarkdowndocstring refers to "those fragments" with no antecedent in its filelens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZATH019SDZY72NDBJT1WKXof review01M3ZAP3N3GZTRF3T09RWJKY6EFixed in
f71add8: the docstring now names PIECES, the length range, and the seed.@ -0,0 +238,5 @@});});// Forgejo's math, definition lists, and heading attributes read code boundaries differently// from CommonMark; each input put a live tag on Forgejo while CommonMark saw it as code.low — Test-group comment narrates past incidents ("each input put a live tag on Forgejo") and repeats the
neutralizeHtmldocstringlens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZAV0S8D8ASCR4CGW9KKDP7of review01M3ZAP3N3GZTRF3T09RWJKY6EFixed in
f71add8: removed both restating comments; the describe titles carry the meaning.@ -0,0 +54,6 @@/** A literal range that would carry a live `<`, rewritten so it no longer does. Code is* re-emitted as HTML with its content escaped, so it still shows verbatim but stays text* however the renderer reads the surrounding markdown. A block is one line: an HTML block* opened by `<pre` ends at the line holding `</pre>`, so no container prefix or blank line* inside it can matter. */low —
rewritedocstring says "A block is one line" as if describing the input, when it means the re-emitted<pre>is written on one linelens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZAV0CBZ8B0Y0XW09D6KYV3of review01M3ZAP3N3GZTRF3T09RWJKY6EFixed in
f71add8: the docstring says the block is re-emitted on one line with newlines written as .@ -0,0 +130,7 @@return segments;}/** Untrusted markdown with no `<` left that could open raw HTML, in prose, code, or link* destinations, so it holds whatever the renderer decides is code: Forgejo's math,* definition lists, and heading attributes draw code boundaries where CommonMark does not.* The parser only keeps code and destinations intact where the two agree. */low —
neutralizeHtmldocstring states its guarantee through an unresolved "it holds" and credits "the parser" with a decision it does not makelens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZASQ0PX4YGYW51T8KD0EXGof review01M3ZAP3N3GZTRF3T09RWJKY6EFixed in
f71add8: the docstring separates the unconditional escaping guarantee from the conditional verbatim display.@ -0,0 +6,4 @@import { neutralizeSegments } from "./neutralize-html.ts";/** The only HTML `neutralizeHtml` emits: a re-emitted code span or one-line code block. */// The parser already decided the block's indentation stays under four columns.low —
EMITTEDcomment about indentation never says it explains the[ \t]*allowance or which parser it meanslens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZAVCTEWM80GKWRKAGWX3X6of review01M3ZAP3N3GZTRF3T09RWJKY6EFixed in
f71add8: the indentation rationale is folded into the EMITTED JSDoc and names parseLiteralRanges.@ -9,4 +11,5 @@// The claim ids carried by claimMarker(); a conversation with no match is a// Markers count only where the wrapper writes them, at the start of a comment:// one quoted further down a body is text, not a claim on the thread.// The claim id carried by claimMarker(); a conversation with no match is a// human thread and stays untouched. Two claims can share one conversation when// their anchors map to the same display line, so every marker counts.const CLAIM_MARKER_PATTERN = /<!-- review:claim:(?<claimId>\S+) -->/gu;// their anchors map to the same display line, so every comment's marker counts.low — Marker comment above
CLAIM_MARKER_PATTERNruns a file-wide rule into the claim-pattern note, then says "every comment's marker counts" right after "Markers count only ... at the start"lens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZAT2KCRXSVKH3FYY9X7J0Vof review01M3ZAP3N3GZTRF3T09RWJKY6EFixed in
f71add8: the position rule and the conversation-scan scope are now separate paragraphs.@ -11,2 +15,2 @@// their anchors map to the same display line, so every marker counts.const CLAIM_MARKER_PATTERN = /<!-- review:claim:(?<claimId>\S+) -->/gu;// their anchors map to the same display line, so every comment's marker counts.const CLAIM_MARKER_PATTERN = /^<!-- review:claim:(?<claimId>\S+) -->/u;medium — Claim markers are trusted from any comment author, so a forged leading marker makes the wrapper reply to and resolve other people's conversations
lens
general-bug· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZB05PTP3APSD99F4SWWTMHof review01M3ZAP3N3GZTRF3T09RWJKY6EThe gap is real, but this PR did not introduce it and the fix is outside this PR's scope. On main,
claimIdsOfandhasMarkermatched a marker anywhere in any comment, with no author check; this PR only narrows that to a leading marker. The fix is separate security hardening: addusertoPullReviewComment(src/forge/types.ts), parse it in the forge client, and count claim, superseded, and disposition markers in src/reconcile/conversation-markers.ts and the dedup in src/reconcile/inline.ts only whenuser.idis the actions identity, as src/reconcile/summary.ts does. Deferred to a follow-up PR; inf71add8I reworded the comment aboveCLAIM_MARKER_PATTERNso it no longer implies an authorship check.@ -0,0 +130,5 @@return segments;}/** Untrusted markdown rewritten so no `<` can open raw HTML, whether it sits in prose, code,* or a link destination. The guarantee holds wherever the renderer draws code boundaries,low — The sanitizer docstring overstates its no-HTML guarantee
lens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZBG61TKJZF7CRP3KM9ENYRof review01M3ZB9GA1SK1R2DV04KBDND50@ -162,0 +203,4 @@});const bodies = (fake.posted[0] ?? []).map((comment) => comment.body);expect(bodies).toHaveLength(400);expect(htmlBeyondMarker(bodies)).toStrictEqual([]);low — Generated comment test ignores raw code tags from claim text
lens
test-trimming· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZBG07KMHRHGZKYNA49ZZB3of review01M3ZB9GA1SK1R2DV04KBDND50@ -18,3 +17,3 @@claimMarker(claim.id),`**${claim.severity}** — ${claim.title}`,`**${claim.severity}** — ${neutralizeHtml(claim.title, "inline")}`,`lens \`${claim.lens}\` · arm \`${claim.arm}\` · ${tally}${wavering}`,medium — Lens and arm strings can break out of code spans into raw HTML
lens
general-bug· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZBKT44A4QA14168MAH16D3of review01M3ZB9GA1SK1R2DV04KBDND50Acknowledged without a fix: this is round 4 of review on this PR, so only clear, severe, demonstrably real bugs are fixed now.
claim.lensandclaim.armare lens and arm identifiers assigned by the review service, not text from the PR author or the reviewed code, so I found no path for untrusted input to reach them (src/reconcile/inline.ts:19, unchanged from main). Escaping them is reasonable defense in depth against a misconfigured or compromised service. It is deferred to a follow-up PR: incommentBody(src/reconcile/inline.ts), passclaim.lensandclaim.armthrough the same neutralization or reject values containing a backtick, with a test. Verified head:f71add8.Real but small, and deliberately deferred to a follow-up PR: round 4 gates one-line docstring changes. The exact fix, in src/markdown/neutralize-html.ts on the
neutralizeHtmldocstring, is to say that no<from the input can open raw HTML while the function emits its own<code>and<pre><code>elements for rewritten code. The same sentence belongs in j4k/review src/report/neutralize-html.ts. Behavior and safety are unchanged.Real but small, and deliberately deferred to a follow-up PR: it changes test coverage only, and round 4 gates that. The exact fix, in src/reconcile/inline.test.ts (and the matching disposition-body test in src/reconcile/dispositions.test.ts): add a posted-comment assertion that a literal source
<code>or</code>in the title or body stays escaped, or make thehtmlBeyondMarkeroracle in src/markdown/comment-html.fixture.ts distinguish emitted code markup from source tags.@ -0,0 +137,7 @@return segments;}/** Untrusted markdown rewritten so no `<` can open raw HTML, whether it sits in prose, code,* or a link destination. The guarantee holds wherever the renderer draws code boundaries,* including where Forgejo's math, definition lists, and heading attributes diverge from* CommonMark; code and destinations display verbatim only where the two agree. */medium — Document that neutralizeHtml emits its own raw HTML
lens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZD3JQC065APZPKYQTMD7D7of review01M3ZCZEQ4SFS7J2EQ4X9F6YC8@ -17,3 +16,3 @@return [claimMarker(claim.id),`**${claim.severity}** — ${claim.title}`,`**${claim.severity}** — ${neutralizeHtml(claim.title, "inline")}`,low — An unmatched title backtick changes the inline comment metadata
lens
general-bug· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M3ZDCXDZHA984WZGK270VF0Qof review01M3ZCZEQ4SFS7J2EQ4X9F6YC8Duplicate of #105152, real but small, deliberately deferred to a follow-up PR (round 5 fixes only clear, severe bugs). The exact fix, in src/markdown/neutralize-html.ts on the
neutralizeHtmldocstring: say that tag-opening<from the input is neutralized while rewritten code is emitted as the function's own<code>and<pre><code>elements. The same sentence belongs in j4k/review src/report/neutralize-html.ts. No behavior or safety change.Real but cosmetic, so acknowledged without a fix at round 5: an unmatched backtick in a title can pair with backticks in the metadata line below it, which changes only how the provenance line renders. It cannot introduce raw HTML, since the title is neutralized and the code span path is escaped. The title and metadata line layout is unchanged from main. Deferred to a follow-up PR: in
commentBody(src/reconcile/inline.ts), put a blank line between the title and the lens line, or escape backticks in the title, with a test. Verified head:6093b7c.6093b7cc9341cd66132f@ -24,3 +27,1 @@if (claimId !== undefined && !ids.includes(claimId)) {ids.push(claimId);}const claimId = CLAIM_MARKER_PATTERN.exec(comment.body)?.groups?.claimId;medium — A user-authored claim marker makes the projector modify a human thread
lens
general-bug· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M41CC43F61YQR67CV4RS6S2Wof review01M41BXJNPMEM7K4YTGYH71DHG@ -162,0 +201,6 @@),),});const bodies = (fake.posted[0] ?? []).map((comment) => comment.body);expect(bodies).toHaveLength(400);expect(htmlBeyondMarker(bodies)).toStrictEqual([]);low — Generated comment test ignores raw code tags from untrusted fields
lens
test-trimming· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M41C69PS48WM3TGG6SQR0V4Zof review01M41BXJNPMEM7K4YTGYH71DHG@ -48,3 +47,3 @@const { claim } = item;const marker = claimMarker(claim.id);if (bodies.some((body) => body.includes(marker))) {// Only a leading marker is one this wrapper wrote; a quoted one is text.low — Leading-marker comment claims authorship that the code never verifies
lens
writing-quality· armdefault· tally 1 valid / 0 invalid / 0 uncertainclaim
01M41C79EYTYDA8PDAJQFSC2ZFof review01M41BXJNPMEM7K4YTGYH71DHGRepeats #105105, which was acknowledged and deferred to a follow-up PR; see the reply in #105129. The marker-author check still belongs in that separate hardening change, not in this PR.
Repeats #105153, which was acknowledged and deferred to a follow-up PR; see the reply in #105157. The extra
<code>assertion belongs in that follow-up, not in this PR.Valid, and deferred to a follow-up. The fix is to reword the comment at
src/reconcile/inline.ts:49so it no longer claims authorship; it is listed in #24.