#6685·awesome-go

avelino/awesome-go site generator enables goldmark html.WithUnsafe and injects raw README HTML into the production homepage as template.HTML, causing stored XSS (CWE-79)

Author: r20z19Created Sep 11, 2026Updated Sep 11, 2026

avelino/awesome-go site generator enables goldmark html.WithUnsafe and injects raw README HTML into the production homepage as template.HTML, causing stored XSS (CWE-79)

Last saved at 2026-09-11

Asset

The static site generator embedded in the avelino/awesome-go repository (SOURCE_CODE) — pkg/markdown/convert.go, main.go, tmpl/index.tmpl.html — whose output is auto-deployed to the production site homepage https://awesome-go.com/ by .github/workflows/site-deploy.yaml.

  • Repository: https://github.com/avelino/awesome-go
  • Audited baseline: main branch HEAD commit 1ed3a46319b9e85d2fedcacc8cece2e0faf456b6. The project has no formal versioned releases, so the affected code is delineated by commit: every version since goldmark's html.WithUnsafe() and the template.HTML pass-through were introduced (including the audited HEAD) is affected.
  • Deployment chain: push to mainsite-deploy.yaml runs go run . → Netlify (production-deploy: true) → https://awesome-go.com/.

Weakness

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (cwe-79)

Description

Version declaration: The audited source is avelino/awesome-go main branch HEAD 1ed3a46. All code references below were verified against the actual repository tree in this submission. This report is prepared in accordance with the platform Policy and Disclosure Guidelines. The finding is code-level confirmed: the source chain is closed end-to-end, and a poisoned homepage artifact produced by the real build pipeline is attached as the white-box logic reproduction result; no live production request testing was performed.

Summary

The homepage generation function renderIndex converts README.md to HTML via goldmark and then explicitly wraps the output as template.HTML before handing it to html/template. html/template treats values of type template.HTML as "trusted HTML" and skips all context-aware escaping for them; meanwhile, the goldmark renderer is configured with html.WithUnsafe(), which allows arbitrary inline HTML in README to pass through verbatim. Two bypasses compose into a fully open channel: any contributor whose PR reaches README.md can plant live markup (<script>, <img onerror>, ...) anywhere in the README — in entry descriptions, top-level paragraphs, etc. — and have it execute on the highest-traffic page of the site, the production homepage, for every visitor. The site sets no CSP.

Finding A: goldmark Unsafe mode passes README inline HTML through verbatim

File: pkg/markdown/convert.go:16-38, key lines convert.go:22-25

goldmark.WithRendererOptions(
    html.WithXHTML(),
    html.WithUnsafe(), // allow inline HTML   ← convert.go:24
),

html.WithUnsafe() is the option goldmark's documentation explicitly warns against enabling for untrusted input: once enabled, raw inline HTML in the README (<script>alert(20)</script>, <img src=x onerror=alert(2)>) is no longer escaped to text but is output byte-for-byte as-is into the conversion result.

Finding B: the template.HTML wrapper actively bypasses html/template context escaping

File: main.go:407-436 (renderIndex), key lines main.go:424-427; sink tmpl/index.tmpl.html:80

data := map[string]interface{}{
    "Body": template2.HTML(body),      // main.go:425 — explicitly marked "trusted"
}
if err := tpl.Lookup("index.tmpl.html").Execute(f, data); err != nil {
<main>
    <div id="content">
        {{.Body}}                       <!-- index.tmpl.html:80 -->
    </div>
</main>

template2 "html/template" (imported at main.go:10) would normally provide context-aware escaping; but at main.go:425 the goldmark output is converted to template.HTML, after which html/template skips escaping by type-system convention and injects it verbatim into {{.Body}}. Even without WithUnsafe, the type wrapper alone would constitute an "developer-declared-trusted" bypass; combined, the README becomes a direct HTML injection channel into the production homepage.

Finding C: no downstream sanitization, and the homepage is the highest-value attack surface

The renderIndex output out/index.html is published by Netlify as the site root page. The subsequent rewriteLinksInIndex pass (main.go:358-404) only rewrites anchor href fragments and never touches body content. README.md is a file any contributor can modify; once the payload is merged it covers the single entry page of the whole site — the page with the most visitors.

Versions Verified

Version goldmark WithUnsafe template.HTML pass-through Live payload in artifacts
avelino/awesome-go main @ 1ed3a46 (audited tree) Present (convert.go:24) Present (main.go:425index.tmpl.html:80) Confirmed (see below)

The poisoned homepage built locally with the real generator carries the payloads verbatim (evidence file workdir/poc/env_awesome-go/out/index.html:86-87). The defect is a structural configuration issue and is expected to affect every historical version since the two switches were introduced; no fix commit was found in the audited tree.


Steps To Reproduce

Pre-conditions: An attacker can open a PR modifying README.md in avelino/awesome-go; the PR is squash auto-merged once quality checks pass (.github/workflows/pr-quality-check.yaml:131-147), and site-deploy.yaml builds and deploys automatically. The site sets no CSP.

Step 1 — submit a PR with bare HTML. Insert anywhere in the README (e.g. under a category heading, inside an entry description):

<img src=x onerror=alert(2)>

<script>alert(20)</script>

Step 2 — merge triggers the build. CI runs go run .: renderIndex reads the README (main.go:408-413) → markdown.ToHTML passes inline HTML through verbatim under WithUnsafe (convert.go:24) → wrapped as template2.HTML(body) (main.go:425) → injected at index.tmpl.html:80 → Netlify publishes it as https://awesome-go.com/.

Step 3 — visit the homepage and observe execution. Expected: <img onerror> and <script> appear in the homepage HTML as real elements and execute on page load. Actual (derived from source logic; recorded artifact from the real local build at workdir/poc/env_awesome-go/out/index.html:86-87):

<img src="x" onerror="alert(2)"/>
<script>alert(20)</script>

Both are unescaped, unencoded live elements (<script> is an executable script element, not text).

Reproduction note: The above is a white-box logic reproduction result — the README inline HTML → goldmark WithUnsafe pass-through → template.HTML bypass → homepage {{.Body}} → production site call chain is proven segment by segment from the source; payload survivability is asserted verbatim against artifacts of the real build (two homepage checks in workdir/poc/verify_out.py PASS; run log workdir/poc/poc_output.txt). Reproduction script: workdir/poc/build_poc_site.sh; attacker input: workdir/poc/malicious_README.md (V2 section).


Impact

Aspect Detail
Attack requirement One merged PR (routine for a community-curated project; auto-merge)
Permission boundary No special privileges needed; the payload sits on the production homepage and executes automatically for every visitor (anonymous included) — no interaction with any entry required
Confidentiality Script in the homepage context can read same-origin page data and exfiltrate it; the site entry page means essentially every visitor is covered
Integrity Full control of homepage presentation: content replacement, fake announcements, phishing redirects, keylogger/crypto-miner injection, SEO poisoning (the homepage carries the highest search weight)
Availability The script can render the homepage unusable (forced redirects, pop-up loops), affecting the entire site's entry
Severity Medium. CVSS 3.1 AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N5.9 (PR:L because one merged PR is required; UI:N because visitors are exploited merely by browsing the homepage). The homepage is the highest-traffic page of the site, making the practical exposure the largest among these findings
CVE eligibility Yes. Attacker-controlled input (any README location), a clear root cause (double bypass: WithUnsafe + template.HTML), a concrete dangerous sink (homepage {{.Body}}), and a clear impact boundary (all visitors) — an independent vulnerability of the site generator product itself
Application methods ① Channel designated by SECURITY.md: https://github.com/avelino/awesome-go/issues/new (public issue; the scope explicitly covers the static site generator); ② maintainer email [email protected] (mailto:[email protected]); ③ VulDB: https://vuldb.com/?submit (login required); ④ MITRE CNA-LR CVE form: https://mitre.github.io/mitre-cve-roles/cve-id-request/ (cveform.mitre.org now 302-redirects there). GitHub Private Vulnerability Reporting was verified via the API as disabled ({"enabled": false}) and is therefore not listed

Suggested fix: Remove html.WithUnsafe() at convert.go:24, or sanitize the goldmark output against a whitelist (bluemonday); remove the template.HTML wrapper at main.go:425 (keep escaping); add a script-src 'self' CSP on Netlify.