gh-aw: URL allowlist bypass via userinfo @ in the content sanitizer (exfiltration channel)
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...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| github/gh-aw Product | < v0.86.0 | v0.86.0 |
Details and references
### 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.