Skip to content

token: validate response host in GetAppInfo before trusting AppInfo headers#1646

Open
winstoncrooker wants to merge 1 commit into
cloudflare:masterfrom
winstoncrooker:token-validate-getappinfo-host
Open

token: validate response host in GetAppInfo before trusting AppInfo headers#1646
winstoncrooker wants to merge 1 commit into
cloudflare:masterfrom
winstoncrooker:token-validate-getappinfo-host

Conversation

@winstoncrooker
Copy link
Copy Markdown

token.GetAppInfo (token/token.go) stops the redirect chain when the last request URL contains AccessLoginWorkerPath (/cdn-cgi/access/login) but does not check that the host serving the response is a Cloudflare-owned host. Once that predicate fires, the response's CF-Access-Domain header and the request's ?kid= query parameter become authoritative for AppInfo.AppDomain and AppInfo.AppAUD. Both fields flow into GetAppTokenIfExists (the on-disk app-token lookup) and BuildAccessRequest (which places the per-app Cloudflare Access JWT in the Cf-Access-Token header on a request to the host the user invoked cloudflared against).

If a victim invokes cloudflared access curl <attacker-host> or cloudflared access login <attacker-host> against a host whose path namespace includes /cdn-cgi/access/login, the existing predicate accepts that response as authoritative. The victim's per-app JWT is then delivered to the attacker server, where it can be replayed against the legitimate Cloudflare Access edge for the JWT's exp window.

A realistic delivery vector for the URL substitution that does not require any prior compromise: subdomain takeover of a hostname that appears in team documentation. Setup guides commonly contain literal cloudflared access curl https://<internal-name>/ snippets, and <internal-name> later lapses or is misconfigured into an attacker-controlled CNAME.

This PR

Adds an isCloudflareAccessHost helper that matches *.cloudflareaccess.com (case-insensitive, port-tolerant) and gates the AccessLoginWorkerPath branch in GetAppInfo on the response host satisfying it.

The CF-Access-Aud header path on the else if branch is a different code shape (the response host is by definition the protected application's host, not a *.cloudflareaccess.com host) and is out of scope for this PR; it warrants a separate consideration.

Tests

  • TestIsCloudflareAccessHost — unit test over a representative input matrix (positive: subdomains, port-tolerant, case-insensitive; negative: bare cloudflareaccess.com, attacker-host, loopback, label-suffix-but-not-subdomain, empty).
  • TestGetAppInfo_RejectsRedirectFromNonCloudflareHostnet/http/httptest-based integration test that drives GetAppInfo against a server mimicking the attacker shape (302 to /cdn-cgi/access/login?kid=AUD, then 200 with CF-Access-Domain) and asserts the new error fires.

Existing tests in token/ continue to pass.

Reporting context

For transparency rather than as an appeal: I filed this as #3709853 on Cloudflare's HackerOne program. The report was closed Not Applicable by H1 triage on threat-model grounds, with reference to preconditions (victim runs command, CI/CD compromised, local access) that don't map cleanly onto the subdomain-takeover vector. I'm opening this PR as a hardening contribution because the bug-shape concern (substring-only predicate with no host check) is decoupled from the threat-model debate, and the fix is small and localized.

Happy to scope down further, drop the unit-test matrix, or close if you'd rather not narrow this particular path.

…eaders

GetAppInfo stops the redirect chain when the last request URL contains
AccessLoginWorkerPath but does not check the host serving the response.
Once that predicate fires, the response's CF-Access-Domain header and the
request's ?kid= query parameter become authoritative for AppInfo.AppDomain
and AppInfo.AppAUD, which then flow into GetAppTokenIfExists (on-disk
token lookup) and BuildAccessRequest (sends the per-app Cloudflare Access
JWT in Cf-Access-Token to the host the user invoked cloudflared against).

Add an isCloudflareAccessHost helper that matches *.cloudflareaccess.com
(case-insensitive, port-tolerant) and gate the AccessLoginWorkerPath
branch on the response host satisfying it. Add a unit test for the
helper and a httptest-based test that exercises GetAppInfo against a
server mimicking the attacker shape.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant