token: validate response host in GetAppInfo before trusting AppInfo headers#1646
Open
winstoncrooker wants to merge 1 commit into
Open
token: validate response host in GetAppInfo before trusting AppInfo headers#1646winstoncrooker wants to merge 1 commit into
winstoncrooker wants to merge 1 commit into
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
token.GetAppInfo(token/token.go) stops the redirect chain when the last request URL containsAccessLoginWorkerPath(/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'sCF-Access-Domainheader and the request's?kid=query parameter become authoritative forAppInfo.AppDomainandAppInfo.AppAUD. Both fields flow intoGetAppTokenIfExists(the on-disk app-token lookup) andBuildAccessRequest(which places the per-app Cloudflare Access JWT in theCf-Access-Tokenheader on a request to the host the user invokedcloudflaredagainst).If a victim invokes
cloudflared access curl <attacker-host>orcloudflared 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'sexpwindow.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
isCloudflareAccessHosthelper that matches*.cloudflareaccess.com(case-insensitive, port-tolerant) and gates theAccessLoginWorkerPathbranch inGetAppInfoon the response host satisfying it.The
CF-Access-Audheader path on theelse ifbranch is a different code shape (the response host is by definition the protected application's host, not a*.cloudflareaccess.comhost) 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: barecloudflareaccess.com, attacker-host, loopback, label-suffix-but-not-subdomain, empty).TestGetAppInfo_RejectsRedirectFromNonCloudflareHost—net/http/httptest-based integration test that drivesGetAppInfoagainst a server mimicking the attacker shape (302 to/cdn-cgi/access/login?kid=AUD, then 200 withCF-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.