Skip to content

fix(sanitize): preserve Markdown body fidelity on read surfaces - #3177

Merged
SamMorrowDrums merged 2 commits into
mainfrom
sammorrowdrums-sanitization-robustness
Sep 3, 2026
Merged

fix(sanitize): preserve Markdown body fidelity on read surfaces#3177
SamMorrowDrums merged 2 commits into
mainfrom
sammorrowdrums-sanitization-robustness

Conversation

@SamMorrowDrums

@SamMorrowDrums SamMorrowDrums commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Narrow release fix for Markdown fidelity on body-bearing GitHub response fields.

  • keep strict sanitize.Sanitize behavior for short metadata fields such as titles
  • route Markdown/code-bearing content fields through sanitize.Content, preserving source text while removing only unconditional invisible characters
  • cover direct read surfaces and direct JSON-marshaled paths for issue, PR, comment, review, release, project status, sub-issue, and related converter responses
  • preserve read-modify-write behavior by avoiding lossy sanitization of body content

Scope

This PR is limited to the Markdown/body fidelity boundary. Title handling remains strict.

Validation

  • script/lint
  • script/test
  • focused fidelity and converter regression tests in pkg/sanitize and pkg/github

Fixes #2202
Fixes #3165

@SamMorrowDrums
SamMorrowDrums marked this pull request as ready for review September 1, 2026 09:20
@SamMorrowDrums
SamMorrowDrums requested a review from a team as a code owner September 1, 2026 09:20
Copilot AI balanced review requested due to automatic review settings September 1, 2026 09:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The sanitizer still has source-corruption and URL-masking edge cases that weaken its fidelity and security guarantees.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 4 Medium severity

New issues introduced by this change (4)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — This fallback escapes every ampersand, including those inside inline, fenced, and indented code.…
Medium severity pkg/​sanitize/​sanitize.go — The bare-URL detector treats every alphabetic scheme:// token as safely visible, although the…
Medium severity pkg/​sanitize/​sanitize.go — These loops cast individual UTF-8 bytes to runes, so Unicode whitespace is never recognized. With…
Medium severity pkg/​sanitize/​sanitize.go — This lookup is case-sensitive even though URI schemes are case-insensitive. A valid CommonMark…
What changed in this PR

Introduces Markdown-aware sanitization to preserve code-bearing GitHub content while neutralizing hidden constructs.

Changes:

  • Adds Goldmark-based sanitize.Content.
  • Applies it across bodies, comments, releases, commits, and sub-issues.
  • Adds extensive tests, benchmarks, and license metadata.
File Description
pkg/​sanitize/​sanitize.go Implements Markdown-aware sanitization.
pkg/​sanitize/​sanitize_test.go Tests fidelity, safety, and performance.
pkg/​github/​minimal_types.go Applies content sanitization to converters.
pkg/​github/​issues.go Sanitizes issue and sub-issue responses.
pkg/​github/​issues_test.go Tests sub-issue sanitization.
pkg/​github/​repositories.go Sanitizes releases and blame messages.
pkg/​github/​repositories_test.go Tests release and blame behavior.
pkg/​github/​discussions.go Preserves discussion body content.
pkg/​github/​discussions_test.go Updates discussion expectations.
pkg/​github/​projects.go Uses content policy for status updates.
pkg/​github/​sanitize_coverage_test.go Expands policy coverage tests.
go.mod Adds Goldmark dependency.
go.sum Records Goldmark checksums.
third-party/​github.com/​yuin/​goldmark/​LICENSE Adds Goldmark’s license.
third-party-licenses.linux.md Updates Linux licenses.
third-party-licenses.darwin.md Updates macOS licenses.
third-party-licenses.windows.md Updates Windows licenses.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

URL masking can leave adjacent hidden Markdown active and has quadratic behavior on adversarial input.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 2 Medium severity

New issues introduced by this change (2)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — This URL mask extends from any http(s):// occurrence to the end of the whitespace-delimited…
Medium severity pkg/​sanitize/​sanitize.go — This backward scan makes markdownURLMask quadratic on an untrusted single-line body containing…
Issues resolved since last review (4)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — This lookup is case-sensitive even though URI schemes are case-insensitive. A valid CommonMark… View resolved comment
Medium severity pkg/​sanitize/​sanitize.go — These loops cast individual UTF-8 bytes to runes, so Unicode whitespace is never recognized. With… View resolved comment
Medium severity pkg/​sanitize/​sanitize.go — The bare-URL detector treats every alphabetic scheme:// token as safely visible, although the… View resolved comment
Medium severity pkg/​sanitize/​sanitize.go — This fallback escapes every ampersand, including those inside inline, fenced, and indented code.… View resolved comment

Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize.go Outdated
@leuasseurfarrelds247-arch

Copy link
Copy Markdown

2026-08-31T171900.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

URL masking currently rewrites valid relative reference destinations and can exempt malformed HTTP-like text from math neutralization.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 2 Medium severity

New issues introduced by this change (2)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — Valid relative reference-link destinations are not actually masked here. inlineDestinationSpan
Medium severity pkg/​sanitize/​sanitize.go — This accepts any non-whitespace suffix after http:// or https:// without checking that it is a…
Issues resolved since last review (2)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — This backward scan makes markdownURLMask quadratic on an untrusted single-line body containing… View resolved comment
Medium severity pkg/​sanitize/​sanitize.go — This URL mask extends from any http(s):// occurrence to the end of the whitespace-delimited… View resolved comment

Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

URL masking retains security bypasses and quadratic parsing paths for malformed untrusted Markdown.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 3 Medium severity

New issues introduced by this change (3)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — Malformed inline-link candidates make this path quadratic. For `"$hidden$" + strings.Repeat("[x](",…
Medium severity pkg/​sanitize/​sanitize.go — An unbalanced opening parenthesis still returns a valid reference destination. Thus `[x]:…
Medium severity pkg/​sanitize/​sanitize.go — The same URL-mask pass is quadratic for unmatched angle brackets: each < invokes IndexByte
Issues resolved since last review (2)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — This accepts any non-whitespace suffix after http:// or https:// without checking that it is a… View resolved comment
Medium severity pkg/​sanitize/​sanitize.go — Valid relative reference-link destinations are not actually masked here. inlineDestinationSpanView resolved comment
Suppressed comments (2)

pkg/sanitize/sanitize.go:507

  • This unbounded search restarts at every line beginning with [. An input containing $ plus many unterminated definition-like lines (for example, repeated [label\n) causes each call to search the whole remaining document for ]:, yielding quadratic work on untrusted bodies. Limit the search to the reference-label grammar/current line or derive destination spans from the already parsed Markdown AST.
	closing := bytes.Index(source[offset:], []byte("]:"))

pkg/sanitize/sanitize.go:584

  • Scheme-less text is accepted here as an autolink even when it is not one. For example, <$ignore$> is plain CommonMark text, but url.Parse returns an empty scheme and linkDestinationIsSafe accepts it as a relative path, so the URL mask suppresses neutralization of the GitHub math delimiters and leaves ignore render-hidden. Restrict this mask to syntax that actually matches a URI or email autolink, ideally using the parsed AST.
	parsed, err := url.Parse(string(destination))
	if err != nil ||
		(parsed.Scheme != "" &&
			!strings.EqualFold(parsed.Scheme, "http") &&
			!strings.EqualFold(parsed.Scheme, "https") &&
			!strings.EqualFold(parsed.Scheme, "mailto")) {

Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

URL masking can leave hidden Markdown unneutralized, and malformed reference labels can cause quadratic processing.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 2 Medium severity

New issues introduced by this change (2)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — This accepts every destination with an empty parsed scheme, so non-autolink text such as…
Medium severity pkg/​sanitize/​sanitize.go — This skips an arbitrary amount of whitespace, including blank lines, while Goldmark inline links…
Issues resolved since last review (3)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — The same URL-mask pass is quadratic for unmatched angle brackets: each &lt; invokes IndexByteView resolved comment
Medium severity pkg/​sanitize/​sanitize.go — An unbalanced opening parenthesis still returns a valid reference destination. Thus `[x]:… View resolved comment
Medium severity pkg/​sanitize/​sanitize.go — Malformed inline-link candidates make this path quadratic. For `"$hidden$" + strings.Repeat("[x](",… View resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

pkg/sanitize/sanitize.go:514

  • bytes.Index searches the entire remaining document for every line that starts with [. An input containing many unmatched reference-label openings (for example, thousands of "[x\n" lines and no "]:") therefore rescans nearly the same suffix once per line and makes Content quadratic, contrary to the bounded adversarial-work goal. Bound label scanning to Goldmark/CommonMark's label grammar and maximum length rather than searching to EOF.

Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Autolink classification still mishandles valid mailto URIs and invalid email domains, affecting fidelity and hidden-content neutralization.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 2 Medium severity

New issues introduced by this change (2)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — Valid mailto: autolinks are URI autolinks and may contain query parameters with additional…
Medium severity pkg/​sanitize/​sanitize.go — This domain check is looser than the goldmark/CommonMark email-autolink grammar: it accepts empty…
Issues resolved since last review (2)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — This skips an arbitrary amount of whitespace, including blank lines, while Goldmark inline links… View resolved comment
Medium severity pkg/​sanitize/​sanitize.go — This accepts every destination with an empty parsed scheme, so non-autolink text such as… View resolved comment

Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize.go Outdated
@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-sanitization-robustness branch from c37624f to 1c4c824 Compare September 2, 2026 12:58
Comment thread pkg/sanitize/sanitize.go
@SamMorrowDrums SamMorrowDrums changed the title fix(sanitize): preserve visible Markdown content fix(sanitize): preserve Markdown body fidelity on read surfaces Sep 3, 2026
@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-sanitization-robustness branch from 1c4c824 to 89b787e Compare September 3, 2026 07:55
@SamMorrowDrums
SamMorrowDrums requested a balanced review from Copilot September 3, 2026 07:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Direct release and sub-issue response paths lack fidelity regression coverage, and one blame assertion is now misleading.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 4 Medium severity · 1 Low severity

New issues introduced by this change (3)
Severity Finding
Medium severity pkg/​github/​issues.go — None of the sub-issue endpoint tests uses content that distinguishes this helper from a no-op, so…
Medium severity pkg/​github/​repositories.go — The new direct release-response path has no regression coverage: neither existing handler test…
Low severity pkg/​github/​repositories_test.go — The following raw-JSON NotContains(&quot;&lt;script&gt;&quot;) assertion now passes only because encoding/json
Pre-existing issues (2)
Severity Finding
Medium severity pkg/​sanitize/​sanitize.go — This domain check is looser than the goldmark/CommonMark email-autolink grammar: it accepts empty… View comment
Medium severity pkg/​sanitize/​sanitize.go — Valid mailto: autolinks are URI autolinks and may contain query parameters with additional… View comment

Comment thread pkg/github/issues.go
Comment thread pkg/github/repositories.go Outdated
Comment thread pkg/github/repositories_test.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The policy split is consistently applied across the targeted response paths with focused regression coverage.

Review tier: Balanced
Findings: 4 Medium severity · 1 Low severity

Pre-existing issues (5)
Severity Finding
Medium severity pkg/​github/​repositories.go — The new direct release-response path has no regression coverage: neither existing handler test… View comment
Medium severity pkg/​github/​issues.go — None of the sub-issue endpoint tests uses content that distinguishes this helper from a no-op, so… View comment
Medium severity pkg/​sanitize/​sanitize.go — This domain check is looser than the goldmark/CommonMark email-autolink grammar: it accepts empty… View comment
Medium severity pkg/​sanitize/​sanitize.go — Valid mailto: autolinks are URI autolinks and may contain query parameters with additional… View comment
Low severity pkg/​github/​repositories_test.go — The following raw-JSON NotContains(&quot;&lt;script&gt;&quot;) assertion now passes only because encoding/jsonView comment

Route body-bearing response fields through the fidelity-preserving content path while retaining strict title handling and remove only unconditional invisible characters. Cover direct and converter read-modify-write surfaces for issues, releases, comments, discussions, projects, and commits.

Refs #2202, #3165

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 69c5ab30-9815-4c07-8385-11a206e68f66
@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-sanitization-robustness branch from 808697e to 71ba0e8 Compare September 3, 2026 13:20
@SamMorrowDrums
SamMorrowDrums requested a balanced review from Copilot September 3, 2026 13:20
Auto-generated by license-check workflow
@SamMorrowDrums
SamMorrowDrums merged commit 25f11e6 into main Sep 3, 2026
8 of 12 checks passed
@SamMorrowDrums
SamMorrowDrums deleted the sammorrowdrums-sanitization-robustness branch September 3, 2026 13:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Several updated tests currently fail because corresponding converters and direct release handlers still use lossy or no sanitization.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 3 High severity

New issues introduced by this change (3)
Severity Finding
High severity pkg/​github/​repositories_test.go — This new expectation fails: GetLatestRelease still marshals the raw RepositoryRelease at…
High severity pkg/​github/​sanitize_coverage_test.go — This expected-content branch makes the converter regression test fail for every case whose…
High severity pkg/​github/​sanitize_coverage_test.go — This assertion currently fails because newMinimalDiscussionComment still applies…
Issues resolved since last review (5)
Severity Finding
Low severity pkg/​github/​repositories_test.go — The following raw-JSON NotContains(&quot;&lt;script&gt;&quot;) assertion now passes only because encoding/jsonView resolved comment
Medium severity pkg/​github/​repositories.go — The new direct release-response path has no regression coverage: neither existing handler test… View resolved comment
Medium severity pkg/​github/​issues.go — None of the sub-issue endpoint tests uses content that distinguishes this helper from a no-op, so… View resolved comment
Medium severity pkg/​sanitize/​sanitize.go — This domain check is looser than the goldmark/CommonMark email-autolink grammar: it accepts empty… View resolved comment
Medium severity pkg/​sanitize/​sanitize.go — Valid mailto: autolinks are URI autolinks and may contain query parameters with additional… View resolved comment
Suppressed comments (2)

pkg/github/repositories_test.go:5093

  • This new expectation fails because GetReleaseByTag still directly marshals the untouched release at repositories.go:2322; the decoded body therefore includes \u200B. Sanitize the direct response's body with sanitize.Content before marshaling it.
				assert.Equal(t, "<details>Notes</details>", *returnedRelease.Body)

pkg/github/issues.go:2019

  • The added endpoint tests do not protect this fidelity boundary: every body fixture is This is **Markdown**\u200B, which produces the same result under both sanitize.Content and the lossy sanitize.Sanitize. Include angle-bracket source such as <int> in the body fixtures and assert it survives across list/add/remove/reprioritize, so a regression to the sanitizer that caused #2202 is detected.
		issue.Body = github.Ptr(sanitize.Content(*issue.Body))

require.NoError(t, err)
assert.Equal(t, tc.expectedResult.TagName, returnedRelease.TagName)
assert.Equal(t, "First Release", *returnedRelease.Name)
assert.Equal(t, "<details>Notes</details>", *returnedRelease.Body)
Comment on lines +237 to +241
expected := sanitizedText
if tt.content {
expected = sanitizedContentText
}
assert.Equal(t, expected, tt.got())
t.Run("discussion comment body (newMinimalDiscussionComment, used by get_discussion_comments)", func(t *testing.T) {
comment := newMinimalDiscussionComment("id", maliciousText, false)
assert.Equal(t, sanitizedText, comment.Body)
assert.Equal(t, sanitizedContentText, comment.Body)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants