#6687·awesome-go

avelino/awesome-go project page JSON-LD codeRepository field omits JSON escaping, letting contributor-controlled URLs close the script element and inject arbitrary script (CWE-79)

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

avelino/awesome-go project page JSON-LD codeRepository field omits JSON escaping, letting contributor-controlled URLs close the script element and inject arbitrary script (CWE-79)

Last saved at 2026-09-11

Asset

The static site generator embedded in the avelino/awesome-go repository (SOURCE_CODE) — tmpl/project.tmpl.html, main.go, pkg/markdown/convert.go — whose output is auto-deployed to the production site project detail pages https://awesome-go.com/<category>/<slug>/ 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 (including the audited HEAD).
  • Deployment chain: push to mainsite-deploy.yaml runs go run . → Netlify (production-deploy: true) → production site.

Weakness

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (cwe-79). The injection occurs inside a <script type="application/ld+json"> raw-text element and is semantically equivalent to code injection into a script context (cwe-94); the primary classification is 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 poisoned project-page artifacts produced by the real build pipeline (two injection points, cross-confirmed by two payload variants) are attached as the white-box logic reproduction results; no live production request testing was performed.

Summary

The developer of the project detail pages implemented a JSON escaping function jsonEscape (main.go:94-100, based on json.Marshal with the surrounding quotes stripped) and applied it to the Title/Description/CategoryTitle fields in the JSON-LD block — but missed the codeRepository field's {{.URL}} (tmpl/project.tmpl.html:51); the same URL is also injected unescaped into the <a href="{{.URL}}"> attribute at :112. This is an asymmetric sanitization defect: one attacker input, two unescaped sinks. A URL containing " escapes the JSON string; because that JSON lives inside a <script type="application/ld+json"> element (an HTML raw-text element), the injected </script> closes the script element outright, and the attacker's following <script>alert(4)</script> becomes a new executable script element.

Key delivery path: goldmark URL-encodes " inside plain markdown link destinations as %22 (that path was disproven as injectable by a real build); however, html.WithUnsafe() (convert.go:24) allows raw inline HTML anchors in the README, whose attribute values pass through verbatim including HTML entities (&quot; etc.), and goquery's Attr("href") then decodes the entities back to raw characters (main.go:324) — bypassing goldmark's URL-encoding layer. parseRepoURL (main.go:440; url.Parse tolerates "/</>, and a github.com hostname with ≥2 path segments passes per main.go:448-452) lets the payload enter Project.URL intact (main.go:479).

Finding A: the jsonEscape coverage gap — codeRepository and the repo link are both unescaped

Files: tmpl/project.tmpl.html:41-79 (JSON-LD block), main.go:94-100 (jsonEscape implementation)

<!-- tmpl/project.tmpl.html:45-46 — escaped (control) -->
"name": "{{jsonEscape .Title}}",
"description": "{{jsonEscape .Description}}",
...
<!-- tmpl/project.tmpl.html:51 — NOT escaped (vulnerable sink ①) -->
"codeRepository": "{{.URL}}"{{if .Meta}},
...
<!-- tmpl/project.tmpl.html:112 — NOT escaped (vulnerable sink ②) -->
<a href="{{.URL}}" rel="nofollow noopener" target="_blank" class="repo-link">
// main.go:94-100
"jsonEscape": func(s string) string {
    b, _ := json.Marshal(s)
    if len(b) < 2 {
        return ""
    }
    return string(b[1 : len(b)-1])
},

Control evidence from the same build: the Title of the V1 sample (containing <img ...>) is correctly escaped in the JSON-LD as \u003cimg src=x onerror=alert(1)\u003e (workdir/poc/env_awesome-go/out/security-tools/v1-victim-evil-github/index.html:43), proving that jsonEscape exists and works for Title/Description — only .URL was missed.

Finding B: raw inline HTML + goquery entity decoding forms the quote delivery path

Attacker input (a README entry; workdir/poc/malicious_README.md:25):

- <a href="https://github.com/evil/v4-jsonld&quot;&gt;&lt;/script&gt;&lt;script&gt;alert(4)&lt;/script&gt;">V4 Demo</a> - desc

Chain: ① convert.go:24 WithUnsafe outputs the raw inline HTML anchor byte-for-byte; ② during goquery parsing, selLink.Attr("href") (main.go:324) decodes the entities &quot;/&gt;/&lt; back to raw "/>/<; ③ parseRepoURL (main.go:440) accepts URLs containing these characters via url.Parse, and the hostname github.com with ≥2 path segments (evil/v4-jsonld...) passes (main.go:448-452); ④ Project.URL carries the raw double quote and </script> (main.go:479); ⑤ when rendered by text/template (main.go:749), {{.URL}} is written verbatim to :51 and :112; ⑥ the goquery re-serialization (main.go:753-758) sanitizes nothing.

Finding C: the actual form of both injection points in the artifact

Sink ① (JSON-LD, workdir/poc/env_awesome-go/out/security-tools/v4-jsonld-evil-github/index.html:49):

"codeRepository": "https://github.com/evil/v4-jsonld"></script><script>alert(4)</script></head><body>&#34;,

The raw " escapes the JSON string and </script> genuinely closes the ld+json script element (goquery's re-parse proves the context break: the residual JSON degrades to body text re-encoded as &#34;, with a </head><body> recombination appearing); the following <script>alert(4)</script> is an executable script element with no type attribute, located in the head, executing as soon as the browser loads the page.

Sink ② (href attribute, same file :106):

<a href="https://github.com/evil/v4-jsonld"><script>alert(4)</script>&#34; rel=&#34;nofollow noopener&#34; ...

The href attribute terminates at the raw ", and <script>alert(4)</script> is injected as an element and executes. One URL data flow, two unescaped sinks.

Cross-confirmation: artifacts of an independent reproduction workflow, workdir/poc/v4-rawhtml-out/repo5_index.html:49 (entity-encoded variant) and repo6_index.html:49 (single-quoted href with literal quotes variant), show the identical JSON-LD escape and script injection; see workdir/poc/V4_RAWHTML_BREAKOUT_EVIDENCE.md. Secondary injection surface: the category page JSON-LD also renders {{.Title}}/{{.Description}} without jsonEscape (tmpl/category-index.tmpl.html:45-46) — same root cause.

Versions Verified

Version jsonEscape on Title/Description Escaping of codeRepository .URL Escaping of href .URL Live script in artifacts
avelino/awesome-go main @ 1ed3a46 (audited tree) Applied (project.tmpl.html:45/46/68/74) Missing (:51) Missing (:112) Confirmed (see Finding C)

The poisoned project page built locally with the real generator contains an executable <script>alert(4)</script> (in two places). 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 auto-merged once quality checks pass and the site is redeployed. The site sets no CSP.

Step 1 — submit a PR with a raw inline HTML anchor. Add an entry to any category list (HTML entities in the attribute value carry the quote and angle brackets, bypassing goldmark's URL-encoding of markdown link destinations):

- <a href="https://github.com/evil/v4-jsonld&quot;&gt;&lt;/script&gt;&lt;script&gt;alert(4)&lt;/script&gt;">V4 Demo</a> - desc

Step 2 — merge triggers the build. CI runs go run .: WithUnsafe passes the anchor through verbatim (convert.go:24) → goquery entity-decodes the href (main.go:324) → parseRepoURL admits it (main.go:448-452) → Project.URL carries the payload → renderProjects renders project.tmpl.html with text/template, writing {{.URL}} unescaped into :51 (JSON-LD) and :112 (href) → Netlify deploys to https://awesome-go.com/security-tools/v4-jsonld-evil-github/.

Step 3 — visit the project detail page and observe execution. Expected: the JSON string is escaped by ", </script> closes the ld+json element, and the injected <script>alert(4)</script> becomes an executable element that runs on page load with no interaction. Actual (derived from source logic; recorded artifacts from the real local build — see the two quotations in Finding C):

  • workdir/poc/env_awesome-go/out/security-tools/v4-jsonld-evil-github/index.html:49: "codeRepository": "https://github.com/evil/v4-jsonld"></script><script>alert(4)</script>...
  • Same file :106: <a href="https://github.com/evil/v4-jsonld"><script>alert(4)</script>&#34; rel=&#34;... (second injection point)

Step 4 — control group confirms the missing escape is a defect, not a design choice. In the same build, the JSON-LD "name" field of the V1 sample page reads "\u003cimg src=x onerror=alert(1)\u003e" (workdir/poc/env_awesome-go/out/security-tools/v1-victim-evil-github/index.html:43) — jsonEscape works as intended; only .URL was missed.

Reproduction note: The above is a white-box logic reproduction result — the README raw HTML → WithUnsafe pass-through → goquery entity decoding → parseRepoURL admission → text/template dual-sink injection → browser-execution call chain (raw-text script-closing semantics are deterministic HTML behavior) is proven segment by segment from the source; the artifact forms are asserted verbatim against the real build output (both V4 checks in workdir/poc/verify_out.py PASS; run log workdir/poc/poc_output.txt). Reproduction script: workdir/poc/build_poc_site.sh; independent cross-reproduction: workdir/poc/V4_RAWHTML_BREAKOUT_EVIDENCE.md, workdir/poc/v4-rawhtml-out/.


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 a project detail page and executes automatically for every visitor (anonymous included) — no click required
Confidentiality The injected script executes within the awesome-go.com origin and can read same-origin page data and exfiltrate it
Integrity Full control of the project detail page presentation: entry content tampering, phishing redirects, fake download buttons (particularly damaging for a "tool index" site — visitors are in the context of finding and installing tools)
Availability The script can render the detail page unusable (forced redirects, pop-up loops)
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 execution occurs on page load). Full code-injection semantics into a script context (not merely an attribute escape) give the payload maximum freedom
CVE eligibility Yes. Attacker-controlled input (README link attributes), a clear root cause (asymmetric jsonEscape coverage + the WithUnsafe/entity-decode delivery path), concrete dangerous sinks (JSON-LD raw-text context :51 and the href attribute :112), 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: Route .URL/.Title/.Description at project.tmpl.html:51/112 and category-index.tmpl.html:45-46 through jsonEscape/HTML escaping; enforce an http/https scheme allowlist on the final rendered URL (independent of parseRepoURL) and reject "/</> characters; add a CSP as defense in depth.