#6675·awesome-go

awesome-go.com pins github.com/yuin/goldmark v1.6.0: IsDangerousURL entity-encoded scheme bypass lands javascript: links in the generated site (CVE-2026-5160 / GHSA-c97m-vxhj-p7j6 / GO-2026-5320)

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

awesome-go.com pins github.com/yuin/goldmark v1.6.0: IsDangerousURL entity-encoded scheme bypass lands javascript: links in the generated site (CVE-2026-5160 / GHSA-c97m-vxhj-p7j6 / GO-2026-5320)

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 github.com/yuin/goldmark v1.6.0 (go.mod:11). Affected project-side paths: pkg/markdown/convert.go:16-38 (ToHTML, html.WithUnsafe()), main.go:406-436 (renderIndex, writing template.HTML(body) verbatim into out/index.html), main.go:310-356 + tmpl/category-index.tmpl.html:113 (goquery href extraction → {{.URL}} + utm suffix). Affected dependency version: github.com/yuin/goldmark >= 0, < 1.7.17 (fix: 1.7.17, commit cb46bbc4eca29d55aa9721e04ad207c23ccc44f9). Audit baseline: main branch commit 2222bc3e8d6af0a969a37640909413bf259ef235 (2026-09-07).

Weakness

Improper Neutralization of Input During Web Page Generation (Cross-site Scripting) (cwe-79) — on the dependency side, improper neutralization of URL schemes (the IsDangerousURL check runs before character-reference resolution, so the scheme filter is bypassed by entity encoding)

Description

Version declaration: This report targets the avelino/awesome-go main @ 2222bc3e8d6af0a969a37640909413bf259ef235 repository tree together with github.com/yuin/goldmark v1.6.0 (pinned by go.mod); advisory CVE-2026-5160 / GHSA-c97m-vxhj-p7j6 / GO-2026-5320 (affected symbols: renderer/html renderLink, renderImage, renderAutoLink; the advisory JSON is archived as GO-2026-5320.json). The findings are code-level white-box confirmations (both-sides source diff + dependency-level differential repro + full-pipeline twin builds); no live-service requests were performed in this submission.

Summary

goldmark v1.6.0's renderer/html renders links/images by performing the IsDangerousURL prefix check on the raw bytes first, and only resolving character references later when writing, via util.URLEscape(dest, true) — the order is inverted. The raw bytes of javascript&colon;alert(1), javascript&#58;..., and javascript&#x3a;... never start with javascript:, so they pass the prefix check; afterwards &colon;/&#58;/&#x3a; are resolved to : and the executable scheme is written verbatim into the href/src attribute. The fixed v1.7.17 computes dest := util.URLEscape(n.Destination, true) first and only then calls IsDangerousURL(dest) (diff: goldmark-html.go-v1.6.0-vs-v1.7.17.patch).

Two independent project-side facts compound this: ① the production configuration enables html.WithUnsafe() (convert.go:24), which short-circuits the entire check (if r.Unsafe || !IsDangerousURL(...)) — nothing is filtered even without a bypass; ② main.go renderIndex (:406-436) writes the goldmark output verbatim into out/index.html via template.HTML(body), and goquery extractCategory (:310-356) lifts href attributes into the category-index.tmpl.html:113 sink <a href="{{.URL}}?utm_...">, with no project-side scheme validation anywhere on the path. Whether viewed from the dependency's default configuration or the production configuration, attacker README links with entity-encoded javascript: schemes land in the deployed site.

Finding A: v1.6.0 checks before entity resolution — the root of the bypass

Files (dependency side): goldmark v1.6.0 renderer/html/html.go:632-640 (renderLink; same shape renderImage :676-684)

_, _ = w.WriteString("<a href=\"")
if r.Unsafe || !IsDangerousURL(n.Destination) {          // (1) check RAW bytes
	_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))  // (2) char refs resolved only here
}

File: goldmark v1.6.0 util/util.go:667-672

// inside URLEscape:
v = UnescapePunctuations(v); v = ResolveNumericReferences(v); v = ResolveEntityNames(v)

Segment-by-segment: IsDangerousURL (html.go:995-1000) is a raw-byte prefix test; the raw bytes of javascript&colon;alert(1)// do not start with javascript: → judged safe → step (2) URLEscape resolves &colon; to : → the executable scheme lands in the attribute. The fixed version URLEscapes first and then checks, so the entity variants already present a javascript: prefix at check time and are blocked (dest=""). goldmark-v1.6.0-IsDangerousURL-source.txt archives the vulnerable filter source.

Finding B: Two project-side sinks — the WithUnsafe short-circuit and the goquery href re-delivery

Files: pkg/markdown/convert.go:16-38, main.go:406-436, main.go:310-356, tmpl/category-index.tmpl.html:113

// convert.go:22-25
goldmark.WithRendererOptions(
	html.WithXHTML(),
	html.WithUnsafe(), // allow inline HTML — short-circuits the IsDangerousURL check
),
// main.go:424-426 — renderIndex: goldmark output goes verbatim into index.html
data := map[string]interface{}{
	"Body": template2.HTML(body),
}

Segment-by-segment:

  • Sink A (index.html): renderIndex marks the goldmark output as trusted HTML via template.HTML(body); README link destinations are written to out/index.html with no filtering.
  • Sink B (category pages): goquery extractCategory (:324) reads the href attribute (entity-decoded) and text/template writes it into category-index.tmpl.html:113 <a href="{{.URL}}?utm_campaign=awesomego&amp;utm_medium=referral&amp;utm_source=awesomego" rel="noopener nofollow">. The trailing // of javascript:alert(1)// JS-comments out the appended utm query; a click executes.
  • Key fact (recorded honestly): under the production configuration (WithUnsafe) goldmark performs no URL filtering at all — even the plain javascript:alert(4) control passes; the CVE bypass means the dependency is unsafe even in the default safe configuration (without WithUnsafe), and only ≥ 1.7.17 restores a working filter. Both facts are reproduced.

Finding C: Dependency-level differential repro and full-pipeline artifacts

Files (evidence): minrepro-goldmark-v1.6.0-output.txt, minrepro-goldmark-v1.7.17-output.txt, sinkA_index_fragment.txt, sinkB_category_fragment.txt, browser_view_audit.txt

Differential repro (goldmark v1.6.0 invoked through the real project package pkg/markdown.ToHTML vs a twin module at v1.7.17; the emitted HTML is re-parsed with golang.org/x/net/html — the same attribute entity-decoding a browser performs):

payload v1.6.0 safe-mode (IsDangerousURL active) v1.6.0 production (WithUnsafe) v1.7.17 safe-mode
[x](javascript&colon;alert(1)//) href="javascript:alert(1)//" DANGEROUS (bypass) DANGEROUS href="" BLOCKED
[x](javascript&#58;alert(2)//) bypass DANGEROUS BLOCKED
[x](javascript&#x3a;alert(3)//) bypass DANGEROUS BLOCKED
[x](javascript:alert(4)) plain href="" BLOCKED (control) DANGEROUS BLOCKED
![x](javascript&colon;alert(5)) image src="javascript:alert(5)" bypass (renderImage) DANGEROUS BLOCKED

Full-pipeline build (go run ., AWESOME_SKIP_FETCH=1) confirms the deployed artifacts:

  1. Sink A: out/index.html contains <a href="javascript:alert(1)//">, (2)//, (3)//, javascript:alert(4), and <img src="javascript:alert(5)"> (sinkA_index_fragment.txt).
  2. Sink B: out/poc-goldmark-entity-scheme-category/index.html contains <a href="javascript:alert(1)//?utm_campaign=awesomego&utm_medium=referral&utm_source=awesomego" rel="noopener nofollow"> (sinkB_category_fragment.txt); the // comments out the utm suffix and a click executes. The plain control's utm suffix produces a JS syntax error (non-executing) — a differential proving the // suffix is the necessary construction.
  3. browser_view_audit.txt: 9 dangerous URL attributes across the generated site from a browser view (entity-decoded), 0 in the benign twin; benign-vs-poc-category-diff.patch is a 4-line surgical diff.

Versions Verified

Version IsDangerousURL ordering Entity-encoded schemes Project pin status
goldmark v1.6.0 (project pin, go.mod:11) ❌ Raw-byte check first; URLEscape resolves entities after &colon;/&#58;/&#x3a; bypass (link and image symbols) ❌ Falls in the affected range < 1.7.17
goldmark v1.7.17 (fixed, verified in twin module) ✅ URLEscape first, then check ✅ At check time the scheme is already javascript: and is blocked

Steps To Reproduce

Pre-conditions: An attacker opens a README PR and a maintainer merges it; site-deploy.yaml renders and deploys to awesome-go.com on push to main. Victim = any visitor clicking a payload link (or loading the image payload). Local reproduction (white-box logic path, no online services):

Step 1 — dependency-level differential repro. Module A (goldmark v1.6.0, invoked via the real project package pkg/markdown.ToHTML) vs module B (identical code, goldmark swapped for v1.7.17). For each of the six payload classes, go run, re-parse the emitted HTML with golang.org/x/net/html, and print the decoded href/src scheme. Expected: under v1.6.0 every entity variant decodes to scheme javascript (in both safe-mode and WithUnsafe configurations); under v1.7.17 safe-mode href="" (blocked). Archived outputs: minrepro-goldmark-v1.6.0-output.txt / minrepro-goldmark-v1.7.17-output.txt.

Step 2 — full-pipeline reproduction. Run the archived script workdir/repro/reproduce_goldmark_cve5160.sh (Go 1.24.1, AWESOME_SKIP_FETCH=1): it stages a poc copy (README = workdir/poc/README.goldmark-cve5160.poc.md, containing [Named Entity Scheme Link](javascript&colon;alert(1)//), the &#58;/&#x3a; variants, ![Entity Image Payload](javascript&colon;alert(5)), and the plain control) and a benign twin (README.goldmark-cve5160.benign.md), running go run . on each.

Step 3 — inspect generated pages (expected results). out/index.html (sink A) and out/poc-goldmark-entity-scheme-category/index.html (sink B) contain the dangerous href/src listed under Finding C (sinkA_index_fragment.txt, sinkB_category_fragment.txt); browser_view_audit.txt counts 9 dangerous attributes from the browser view and 0 in the benign twin.

Actual vs. expected results: Actual — entity-encoded javascript: schemes bypass IsDangerousURL under v1.6.0's default safe configuration, and under the production configuration (WithUnsafe) they pass unfiltered together with the plain variant; the fixed version blocks the entity variants in both configurations. Expected (secure behavior) — dangerous schemes in README link destinations are filtered to empty/escaped before rendering. The divergence is produced jointly by the dependency's check-ordering defect and the project-side WithUnsafe short-circuit; the chain is closed at source level.


Impact

Aspect Detail
Attack requirement One README PR with entity-encoded javascript: links merged by a maintainer (normal community flow); no server privileges
Privilege boundary The attacker places executable dangerous URLs on the awesome-go.com front page and category pages; the injection lives in CI build artifacts (static HTML)
Confidentiality A visitor clicking a payload link (sinks A/B) — or the browser loading the image payload (<img src="javascript:...">, which some engines surface as navigation) — executes script that can read and exfiltrate same-origin data
Integrity Dangerous URLs enter the public directory link lists (with rel="noopener nofollow" — which does not stop javascript: execution); the attacker can leverage a popular directory to induce clicks
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 (a click is required, UI:R); 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-5160 / GHSA-c97m-vxhj-p7j6 / GO-2026-5320, affecting goldmark < 1.7.17); 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 github.com/yuin/goldmark ≥ 1.7.17 and removal of WithUnsafe); ② 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)

Additional notes:

  • Suggested fix: ① upgrade go.mod:11 to github.com/yuin/goldmark v1.7.17 or later (restores the scheme filter in default configuration); ② remove html.WithUnsafe() at convert.go:24 (in the production configuration it short-circuits all URL filtering — the plain javascript: control also passes); ③ add a project-side scheme allowlist (http/https) covering the post-extraction {{.URL}} sink (category-index.tmpl.html:113).
  • Persistence note: the payload lives in README.md and persists in the deployed site across every push-to-main build until the payload lines are removed — stored.