Summary
The content sanitizer's URL redaction claims to redact any https:// URL whose
host is not on the allowlist, but its host regex stops at @. For
https://github.com@evil.com/, the regex captures github.com (allowlisted) and
the real host evil.com passes unredacted. This defeats the URL-redaction
control and gives an exfiltration channel, including a zero-click variant through
GitHub's camo image proxy when an agent comment renders. It needs no special
config, which gives it the broadest reach of the issues I found in this
review of gh-aw.
I verified this against current main at commit 966ccb8 (2026-07-12). The file
is byte-for-byte identical (blob bcb3256) to the commit where it was first
verified, bdfdb8e (2026-06-24, ~v0.80.9); no commit touched it in between.
Details
sanitizeUrlDomains uses httpsUrlRegex
(actions/setup/js/sanitize_content_core.cjs:268):
/https:\/\/([\w.-]+(?::\d+)?)(\/(?:(?!https:\/\/)[^\s,])*)?/gi
The host capture group ([\w.-]+(?::\d+)?) does not include @, so for
https://github.com@evil.com/ it captures github.com. applyDomainFilter
(:276-311) splits on :, matches the allowlist, and returns the URL unchanged.
The real host is evil.com. The protocol-relative regex at :330 has the same
host group, so //github.com@evil.com/ bypasses too. There is no new URL()
parse of the content on this path; the only new URL() in the file builds the
allowlist from environment variables.
The mention-neutralization sibling is also present. neutralizeAllMentions runs
at :1290, before URL sanitization at :1297, and is not re-run afterward. A
redacted host rewritten to (evil.com/redacted)@maintainer leaves @maintainer
as a live mention.
PoC
sanitizeContent("")
returns byte-identical output; the URL is not redacted. I confirmed this offline
against the exact regex and filter logic, on the bare, markdown-image, and
protocol-relative forms, and on both the incoming
(actions/setup/js/sanitize_incoming_text.cjs) and output
(actions/setup/js/sanitize_content.cjs) paths. A plain https://evil.com/...
with no @ redacts correctly, so the bypass is specific to userinfo. No test
covers @ in the authority.
Impact
Defeats URL redaction and provides an exfiltration channel. Zero-click via
GitHub's camo image proxy when an agent comment containing such a markdown image
is rendered: the proxy issues a server-side GET to evil.com with the query
string. No special config is required.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:N/A:N (High). Suggested vector.
Suggested fix
Parse the authority with new URL() and reject any credentials/@ in it,
instead of the regex. Re-run mention neutralization after URL redaction.
Summary
The content sanitizer's URL redaction claims to redact any
https://URL whosehost is not on the allowlist, but its host regex stops at
@. Forhttps://github.com@evil.com/, the regex capturesgithub.com(allowlisted) andthe real host
evil.compasses unredacted. This defeats the URL-redactioncontrol and gives an exfiltration channel, including a zero-click variant through
GitHub's camo image proxy when an agent comment renders. It needs no special
config, which gives it the broadest reach of the issues I found in this
review of gh-aw.
I verified this against current
mainat commit966ccb8(2026-07-12). The fileis byte-for-byte identical (blob
bcb3256) to the commit where it was firstverified,
bdfdb8e(2026-06-24, ~v0.80.9); no commit touched it in between.Details
sanitizeUrlDomainsuseshttpsUrlRegex(
actions/setup/js/sanitize_content_core.cjs:268):The host capture group
([\w.-]+(?::\d+)?)does not include@, so forhttps://github.com@evil.com/it capturesgithub.com.applyDomainFilter(
:276-311) splits on:, matches the allowlist, and returns the URL unchanged.The real host is
evil.com. The protocol-relative regex at:330has the samehost group, so
//github.com@evil.com/bypasses too. There is nonew URL()parse of the content on this path; the only
new URL()in the file builds theallowlist from environment variables.
The mention-neutralization sibling is also present.
neutralizeAllMentionsrunsat
:1290, before URL sanitization at:1297, and is not re-run afterward. Aredacted host rewritten to
(evil.com/redacted)@maintainerleaves@maintaineras a live mention.
PoC
sanitizeContent("")returns byte-identical output; the URL is not redacted. I confirmed this offline
against the exact regex and filter logic, on the bare, markdown-image, and
protocol-relative forms, and on both the incoming
(
actions/setup/js/sanitize_incoming_text.cjs) and output(
actions/setup/js/sanitize_content.cjs) paths. A plainhttps://evil.com/...with no
@redacts correctly, so the bypass is specific to userinfo. No testcovers
@in the authority.Impact
Defeats URL redaction and provides an exfiltration channel. Zero-click via
GitHub's camo image proxy when an agent comment containing such a markdown image
is rendered: the proxy issues a server-side GET to
evil.comwith the querystring. No special config is required.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:N/A:N (High). Suggested vector.
Suggested fix
Parse the authority with
new URL()and reject any credentials/@in it,instead of the regex. Re-run mention neutralization after URL redaction.