#6674·awesome-go

awesome-go.com pins golang.org/x/net v0.38.0: duplicate-attribute parse/render anomaly tree defeats the "Sanitize HTML" goquery roundtrip (CVE-2026-27136 / GO-2026-5030)

Author: r20z19Created Sep 8, 2026Updated Sep 8, 2026

awesome-go.com pins golang.org/x/net v0.38.0: duplicate-attribute parse/render anomaly tree defeats the "Sanitize HTML" goquery roundtrip (CVE-2026-27136 / GO-2026-5030)

Last saved at 2026-09-08

Asset

avelino/awesome-go — the static site generator that builds awesome-go.com (SOURCE_CODE) and its pinned dependency golang.org/x/net v0.38.0 (go.mod:17, pulled in via goquery v1.8.1 → cascadia). Affected scenario: the two goquery parse-then-serialize roundtrips the generator performs on rendered template output (main.go:223-237, whose source comment is literally labeled "Sanitize HTML"; main.go:753-761). Affected dependency version: golang.org/x/net < 0.55.0 (the project's pin of v0.38.0 falls in range). Audit baseline: main branch commit 2222bc3e8d6af0a969a37640909413bf259ef235 (2026-09-07).

Weakness

Improper Neutralization of Input During Web Page Generation (Cross-site Scripting) (cwe-79) — a parse-tree anomaly faithfully serialized such that browser semantics diverge from the sanitized/normalized result (i.e., the advisory's "sanitize input HTML before rendering" scenario fails)

Description

Version declaration: This report targets the avelino/awesome-go main @ 2222bc3e8d6af0a969a37640909413bf259ef235 repository tree together with golang.org/x/net v0.38.0 (OSV pinned commit 7770ec48d03fec35e378665337b4faca93c38423, fixed version 0.55.0); advisory CVE-2026-27136 / GO-2026-5030: "Invoking duplicate attributes can cause XSS in golang.org/x/net/html", "Parsing arbitrary HTML which is then rendered using Render can result in an unexpected HTML tree. This can be leveraged to execute XSS attacks in applications that attempt to sanitize input HTML before rendering." The findings are code-level white-box confirmations (both-versions source diff + dependency-level minimal repro + full-pipeline twin builds); no live-service requests were performed in this submission.

Summary

awesome-go's generator writes README-derived content through templates and then performs two goquery (i.e., golang.org/x/net/html underneath) parse-then-serialize roundtrips on the output before deployment — one of them carries the source comment "Sanitize HTML" (main.go:221-222). Under x/net v0.38.0, duplicate attributes — a parse error per the HTML5 spec — are all retained in the node and serialized verbatim, while a browser re-parses first-occurrence-wins (case-insensitively) and only honors the first attribute. Any sanitizer/normalizer assuming "later value overrides earlier" (e.g., the map-normalization pattern used by x/net's own copyAttributes, parse.go:850-862) or "survivors after dedup" mis-trusts the safe value while the dangerous first attribute wins in the browser. The project pins v0.38.0, so duplicate-attribute payloads embedded in README inline HTML (admitted by goldmark WithUnsafe, entity-decoded by goquery .Text(), unescaped by text/template) sail through the self-described sanitizing roundtrip into the deployed pages.

Finding A: x/net v0.38.0 retains duplicate attributes; v0.55.0 fixes to first-value-only

Files (dependency side): x/net/html tokenizer.go (v0.38.0 readTag appends every z.pendingAttr into z.attr with no dedup) vs v0.55.0 (adds attrNames map[string]bool, dedups by lowercased key, keeps only the first occurrence). Fix diff: xnet-token-go-fix-diff.patch.

Files (project side): main.go:221-237, main.go:739-770

// main.go:221-237 — renderCategories: goquery roundtrip, source comment says "Sanitize HTML"
// Sanitize HTML. This is not necessary, but allows to have content
// of all html files in same style.
{
	doc, err := goquery.NewDocumentFromReader(buf)
	...
	html, err := doc.Html()   // x/net/html Parse + Render roundtrip
	...
	os.WriteFile(categoryIndexFilename, []byte(html), 0644)
}

Segment-by-segment: doc.Html() follows the x/net/html Parse → Render path. The v0.38.0 parser's addElement (parse.go:325-332) carries p.tok.Attr into the node verbatim — the node holds all duplicate attributes; html.Render serializes all of them. The HTML5 spec requires the tokenizer to flag duplicate attribute names as a parse error and keep only the first (first-occurrence-wins, case-insensitive). The v0.38.0 roundtrip output therefore diverges semantically from what any browser produces by re-parsing — the roundtrip is neither idempotent nor normalizing. The attack input chain matches the established README pipeline: community PR edits README → entity-encoded inline HTML (admitted by goldmark WithUnsafe) → goquery extractCategories .Text() decodes entities → text/template writes unescaped → the roundtrip parses with v0.38.0 → the anomaly tree is faithfully serialized into the deployed out/ pages → the browser executes the first dangerous attribute.

Finding B: Dependency-level minimal repro — a "last-wins-map" sanitizer is bypassed

Files (evidence): dependency-level/main.go, dependency-level/output_v0380.txt, dependency-level/output_v0550.txt

v0.38.0 (verbatim from output_v0380.txt):

=== Payload P1 ===
input:  <a href="javascript:alert(1)" href="https://benign.example/safe">click</a>
parsed tree attrs (v<ver> parser): [{a@href javascript:alert(1)} {a@href https://benign.example/safe}]
Render output:                     <a href="javascript:alert(1)" href="https://benign.example/safe">click</a>
browser re-parse (first-wins):     [a@href=javascript:alert(1)]
last-wins-map sanitizer: blocked=false, emits [{a@href https://benign.example/safe}]
verdict: *** BYPASS: sanitizer last-wins saw benign value, browser honors DANGEROUS first attr ***

Segment-by-segment: the parse tree holds both hrefs; Render emits both; a first-wins browser executes javascript:alert(1); a last-wins-map sanitizer sees the benign https://benign.example/safe and sets blocked=falsethe sanitizer and the browser see different trees. The mixed-case HREF variant hits identically (P2). For <img src=x onerror="alert(1)" onerror="alert(2)">, both event handlers enter the tree/serialization and the value kept by a map sanitizer diverges from the value the browser executes (smuggling differential, P3). Under fixed v0.55.0 (output_v0550.txt): only the first attribute survives into the tree, the roundtrip is idempotent, the sanitizer sees the true first value and blocks it — 3/3 payloads verdict = blocked, no bypass.

Finding C: Full-pipeline twin builds — the anomaly tree lands in deployed pages and vanishes with the dependency upgrade

Files (evidence): poc_projectA_duphref_anchor.txt, poc_projectB_duponerror_img.txt, poc_category_duphref_anchor.txt, the fixed_* counterparts, poc-vs-fixed-site-diff.patch, summary.json

PoC README (workdir/poc/README.cve27136.md), two payloads:

<a href="javascript:alert(27136)" href="https://benign.example/safe">dup-attr-a</a>
<img src=x onerror="alert(1)" onerror="alert(2)">
  • Vulnerable build (go.mod identical to upstream, x/net v0.38.0): both the project page and the category page (both roundtrips) serialize <a href="javascript:alert(27136)" href="https://benign.example/safe"> and <img src="x" onerror="alert(1)" onerror="alert(2)"/> — the anomaly tree is faithfully serialized into the generated pages (summary.json, 6/6 assertions pass).
  • Fixed twin build (only x/net upgraded to 0.55.0 plus its companion modules; diff of the two go.mod files confirms no other change): the same README yields only the first attribute <a href="javascript:alert(27136)"> / <img src="x" onerror="alert(1)"/> — duplicate attributes disappear entirely, pinning the root cause to the x/net version.
  • poc-vs-fixed-site-diff.patch provides a byte-level comparison of the two builds (the static-attribute ordering differences in the diff are new-serializer behavior, unrelated to this CVE's security semantics).

Versions Verified

Version Duplicate-attribute handling Roundtrip output Sanitizer semantics
golang.org/x/net v0.38.0 (project pin, go.mod:17) ❌ All retained (z.attr = append(...), no dedup) ❌ All duplicates serialized ❌ Roundtrip non-idempotent; diverges from browser first-wins
golang.org/x/net v0.55.0 (fixed, verified in twin build) ✅ First value only (attrNames dedup) ✅ Idempotent ✅ Sanitizer sees the same tree as the browser

Advisory affected range golang.org/x/net < 0.55.0; the project falls in range. No upgrade commit was found in the audited tree.


Steps To Reproduce

Pre-conditions: An attacker opens a README PR containing entity-encoded duplicate-attribute inline HTML and a maintainer merges it; CI (tests.yaml/site-deploy.yaml both run go run .) builds and deploys. Local reproduction (white-box logic path, no online services):

Step 1 — dependency-level minimal repro. Use the archived dependency-level/ module (main.go + go.mod, defaulting to x/net v0.38.0): go run . with the three payloads starting from <a href="javascript:alert(1)" href="https://benign.example/safe">click</a>; then in a twin module run go get golang.org/x/[email protected] && go run . as the fixed control. Expected: under v0.38.0 the tree holds all duplicates, Render serializes them verbatim, first-wins browser semantics execute the first value while a last-wins-map sanitizer passes (output_v0380.txt: P1/P2 verdict BYPASS); under v0.55.0 only the first value survives and the sanitizer blocks (output_v0550.txt: 3/3 blocked).

Step 2 — full-pipeline reproduction. Run the archived script workdir/repro/reproduce_cve2026_27136.sh (AWESOME_SKIP_FETCH=1, GOPROXY pinned to a mirror): it stages two source copies — poc (x/net v0.38.0) and fixed (only x/net upgraded to 0.55.0) — both with the README workdir/poc/README.cve27136.md (payload A &lt;a href="javascript:alert(27136)" href="https://benign.example/safe"&gt;dup-attr-a&lt;/a&gt; and payload B &lt;img src=x onerror="alert(1)" onerror="alert(2)"&gt;), and runs go run . on each.

Step 3 — inspect generated pages (expected results). Vulnerable build: out/poc-dup-attr-category/poc-repo-dup-a-poc-owner-github/index.html (project page) and out/poc-dup-attr-category/index.html (category page) both contain <a href="javascript:alert(27136)" href="https://benign.example/safe"> (poc_projectA_duphref_anchor.txt, poc_category_duphref_anchor.txt) and <img src="x" onerror="alert(1)" onerror="alert(2)"/> (poc_projectB_duponerror_img.txt). Fixed build: the same README yields only the first attribute (fixed_* files). Byte-level diff: poc-vs-fixed-site-diff.patch; assertion results: summary.json (6/6 pass).

Step 4 — browser-semantics cross-check. Re-verify under HTML5 tokenizer rules: duplicate attributes are a parse error and the browser DOM keeps only the first occurrence (case-insensitive) — i.e., javascript:alert(27136) and onerror="alert(1)" take effect — diverging from the "all attributes coexist" form serialized into the generated page, confirming the roundtrip is non-normalizing.

Actual vs. expected results: Actual — the self-described "Sanitize HTML" goquery roundtrip, under x/net v0.38.0, writes the spec-violating duplicate-attribute structure verbatim into deployed pages; the fixed twin build, on identical input, produces roundtrip output consistent with browser semantics. Expected (secure behavior) — the roundtrip should yield a normalized tree (first duplicate only) so the sanitization assumption holds. The single varying variable between the two builds is the x/net version (verified via diff of both go.mod files); the chain is closed at source level.


Impact

Aspect Detail
Attack requirement One README PR with duplicate-attribute payloads merged by a maintainer (normal community flow); same attack channel as the README-injection findings, but the root cause lives in the dependency
Privilege boundary The attacker causes pages deployed to awesome-go.com to carry attribute structures whose browser semantics diverge from the normalized result; no server privileges involved
Confidentiality The browser honors the first dangerous attribute (click-executed javascript: href, dual-onerror smuggling): same-origin script execution can read and exfiltrate same-origin data
Integrity Bypasses the project's own "Sanitize HTML" roundtrip; any downstream relying on that roundtrip to disambiguate attributes (cache/rewrite/minify pipelines) receives a tree inconsistent with browser semantics
Availability No server-side impact
Severity Medium. Advisory CVSS 3.1 AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N; in this project's context the attacker first needs a merged PR (PR:L), netting ≈ 5.0–5.4
CVE eligibility The root-cause CVE already exists (CVE-2026-27136 / GO-2026-5030, affecting x/net < 0.55.0); no new CVE ID is needed. This report serves as affected-consumer evidence for urging the project to upgrade
Suggested submission channels ① GitHub Issue per SECURITY.md (https://github.com/avelino/awesome-go/issues/new, with the upgrade recommendation golang.org/x/net ≥ 0.55.0); ② maintainer email [email protected]; ③ VulDB (https://vuldb.com/?submit, login required, may reference the existing CVE); ④ MITRE CNA-LR (https://mitre.github.io/mitre-cve-roles/cve-id-request/, fallback, only if coordination is needed)

Additional notes:

  • Suggested fix: upgrade go.mod:17 to golang.org/x/net v0.55.0 or later (with companion upgrades of x/sys and x/text; the twin build verifies the behavioral difference). After the upgrade the "Sanitize HTML" roundtrip output is idempotent and consistent with browser semantics. Long term, migrate the template stack to html/template to remove reliance on roundtrip sanitization.
  • Relation to the text/template injection: independently valid — even if the template layer is later fixed to escape, as long as the dependency stays at v0.38.0 the "sanitizing" roundtrip still fails to normalize duplicate attributes (proven by the fixed twin build: with the dependency fixed, roundtrip output matches browser semantics).