Skip to content

fix(security): harden OAuth and fetch_tags redirects, keep region auto-discovery - #521

Merged
carlosmmatos-cs merged 7 commits into
CrowdStrike:mainfrom
carlosmmatos:fix/post-520-redirect-hardening
Sep 8, 2026
Merged

carlosmmatos-cs merged 7 commits into
CrowdStrike:mainfrom
carlosmmatos:fix/post-520-redirect-hardening

Conversation

@carlosmmatos

@carlosmmatos carlosmmatos commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Follow-on to #520.

The OAuth token POST carries the client secret in the request body, and a 307 or 308 replays that body to whatever Location names. Stripping headers does not help, because the secret is not in a header. So the four bash token requests no longer pass -L, and the three PowerShell ones set -MaximumRedirection 0.

That redirect is not a failure, though — it is how region auto-discovery works. A token request sent to the wrong region comes back as a 308 with an x-cs-region header naming the right one. Measured against the live API on curl 7.29.0 and 7.76.1: the un-followed response does carry x-cs-region, and following it with -L does return a token, which is the proof that the body gets replayed.

Removing the redirect on its own therefore broke discovery on both platforms. In bash, with FALCON_CLOUD unset the scripts died with "Unable to obtain CrowdStrike Falcon OAuth Token", and with FALCON_CLOUD set to a region the credentials do not live in they died instead of warning. Windows PowerShell 5.1 broke too: a 3xx is returned there rather than thrown, so ConvertFrom-Json received the redirect body as a Byte[] and failed with "Cannot convert 'System.Byte[]' to the type 'System.String'", which the catch then reported as an unhandled error. The region logic was never reached. PowerShell 7 was unaffected, because it throws.

Each bash script now reads x-cs-region off the un-followed response and re-issues the request itself. The region is resolved through cs_cloud(), a closed allowlist that dies on anything it does not know, so the credential only ever goes to a host the script chose, never to one the redirect named. cs_cloud() takes the region as an optional argument for that, defaulting to the current one, so every existing call site is unchanged. The retry writes its headers to a separate file so the existing region-hint block still reads the first response, and the payload still reaches curl on stdin rather than in argv. The resolved host is checked before use rather than relying on cs_cloud()'s die to unwind, because exiting a nested command substitution ends the script under dash but not under bash.

Invoke-FalconAuth now recognises a 3xx on the success path as well as in the catch, and funnels both into one place so 5.1 and 7 take the same route. It reads the header through a small helper, because the collection type differs: 5.1 gives a WebHeaderCollection with only a string indexer, while 7 gives HttpResponseHeaders, where that indexer silently returns empty instead of failing.

fetch_tags keeps its earlier change of no -L, and the now-dead --proto-redir has been removed with it, so the convention is uniform: --proto-redir appears on exactly the curl invocations that pass -L. That path was measured end to end with a pass-through curl wrapper — the registry token request, the tags list, the ccid call and the image-registry-credentials call all answer 200 in a single hop, so nothing there wants a redirect. That was against registry.crowdstrike.com; the gov registries were not reachable from the test environment.

Two pre-existing issues turned up while checking this and are tracked separately rather than folded in: curl_command() still passes -L with the bearer token attached (#523), and handle_curl_error is skipped for the token request when a script is run as sh rather than bash, because set -e aborts on the failed command substitution first (#524). The second reproduces identically on the base commit, so it is not introduced here.

… replay

Stop bash OAuth token POSTs and PowerShell Invoke-FalconAuth from replaying
client_secret on HTTPS redirect hop 2. Pin fetch_tags to HTTPS without -L.
Strip Authorization before following Falcon download CDN redirects.

Follow-on to CrowdStrike#520. Live-validated on the fork (CAND-001/002/003/004).
@carlosmmatos
carlosmmatos requested a review from a team as a code owner September 8, 2026 14:01

@carlosmmatos-cs carlosmmatos-cs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this out and exercised the redirect paths against a local HTTPS origin + CDN pair (self-signed cert, SkipCertificateCheck) on PowerShell 7.6.5, plus curl 8.7.1 for the bash side. Notes below, most important first.

The OAuth half is correct, and I can show it closes a real leak

With the pre-PR settings, a 307 on the token POST replays the request body to the redirect target. Measured:

(8443, '/oauth2/token', 'Bearer ...', 'client_id=ID123&client_secret=BODYSECRET')
(8444, '/evil/token',   None,        'client_id=ID123&client_secret=BODYSECRET')   <- secret replayed

With -MaximumRedirection 0 the second hop is never contacted at all. Worth noting why this matters: the Authorization header was already stripped by the platform on that hop (None above), so header stripping buys nothing here — the secret is in the body, and the body is replayed on 307/308. So -MaximumRedirection 0 and the bash -L removal are exactly the right fix, and the same reasoning carries to all four bash token POSTs. No objection to any of that.

The bash side also gets a small bonus I do not think was intentional: without -L, --dump-header captures one response instead of several, so the x-cs-region hint parse (grep -i ^x-cs-region: | head -n 1) can no longer pick up a later hop's headers.

The Invoke-FalconDownload rewrite does not close anything

On the download the credential is a header, not a body, and both platforms already strip it on redirect:

  • PS 7, measured, running main's pre-PR code with automatic following: the CDN hop received Authorization: None. I tried it cross-origin (8443 -> 8444) and same-origin; it strips in both cases.
  • Windows PowerShell 5.1: the AllowAutoRedirect docs state "The Authorization header is cleared on auto-redirects."

So the ~90 added lines give the same observable result as the code they replace. The manual follow does work — I confirmed hop 1 gets the bearer, hop 2 does not, non-auth headers survive, and the payload lands — but it is re-implementing something the HTTP stack already does.

And on Windows PowerShell 5.1 I believe the download now fails

Both scripts declare #Requires -Version 3.0, so 5.1 is a supported target. From the same doc page:

If AllowAutoRedirect is set to false, all responses with an HTTP status code from 300 to 399 is returned to the application.

-MaximumRedirection 0 sets AllowAutoRedirect = $false. On 5.1 a 302 is therefore returned, not thrown, so the entire new catch block never runs. -OutFile writes the 302 body to the installer path, and the run then dies at the hash comparison (falcon_windows_install.ps1:641). That is a loud failure rather than a bad install — the SHA256 check does its job — but it is still a broken install on the default Windows shell, where the pre-PR code worked.

Two more 5.1 problems in the same block, if the catch ever did run: $_.Exception.Response is an HttpWebResponse there, whose .Headers is a WebHeaderCollection. That type has no .Location property and no .Contains(string) method, so both branches of the Location lookup fail. Those two lines are PS 7 only.

I could not execute any of this on 5.1 — this part is read off the documentation, not measured, so please treat it as a question rather than a finding. A single run on a real Windows PowerShell 5.1 host would settle it. Given the section above, the simplest resolution may be to drop the download rewrite and keep the OAuth changes.

curl_command() still pairs Authorization: Bearer with -L

The PR title says "download redirects", but the bash download is untouched. curl_command() keeps -L in all four scripts (falcon-linux-install.sh:712, falcon-linux-uninstall.sh:258, falcon-linux-migrate.sh:238, falcon-container-sensor-pull.sh:293), and the installer download at falcon-linux-install.sh:418 goes through it with the bearer injected via -K-.

curl 8.7.1 strips that header on a cross-host redirect (I verified), so current curl is fine. The exposed band is 7.30 through 7.57 — 7.29.0 and 7.58.0+ both strip. That covers Ubuntu 16.04 (7.47), Debian 8/9 (7.38/7.52), SLES 12 (7.37) and Amazon Linux 1. All EOL, but all plausible sensor targets. verify_sha256 at :422 bounds the damage to token exposure rather than a tampered installer. Not necessarily this PR's job, but the asymmetry with the PowerShell change is worth a decision either way.

Two smaller things

The follow request does not bound or re-pin the hops after hop 2. It runs with default redirect handling, so I measured about 50 further auto-followed hops against a server that redirects in a loop. No credentials on any of them and the hash check holds, so impact is low — but the scheme pin you just validated is handed straight back to the default policy, and on 5.1 AllowAutoRedirect will follow HTTPS -> HTTP. -MaximumRedirection 0 (or 1) on the follow call would keep the guarantee explicit.

In falcon_windows_migrate.ps1, $WebRequestParams only ever holds Proxy (:1191, :1216) and never Headers, so the if ($key -eq 'Headers') strip loop is unreachable. It is also a latent trap: if anyone later adds Headers there, Invoke-WebRequest @FollowParams ... -Headers $strippedHeaders binds Headers twice and fails with "specified more than once". Merging into one header hashtable before the call would remove both issues.

What I verified as passing

Lint is clean. All four gates from CLAUDE.md — shfmt -i 4 -ci -ln bash, shfmt -i 4 -ci -ln posix, shellcheck, shellcheck --shell dash — produce no output on all four bash files.

Behaviour on PS 7.6.5:

Case Result
Direct 200, no redirect Works, correct payload
HTTPS -> HTTPS cross-host 302 Follows, correct payload, hop 2 gets no Authorization, X-Extra preserved
HTTPS -> HTTP 302 Refused before the HTTP hop is contacted (the InsecureRedirection branch is load-bearing on 7.4+)
307 on OAuth POST Hop 2 never contacted

Failure paths on the bash side are safe: a 3xx now yields an empty token and a clear die (falcon-linux-install.sh:794), and fetch_tags dies on an empty bearer.

One last nit: --proto-redir '=https' is inert wherever -L was removed. Harmless as defence in depth, and your comment does explain it, but a future reader may read it as evidence that redirects are handled.

…ect hardening

Co-authored-by: Carlos Matos <carlosmmatos@users.noreply.github.com>
@carlosmmatos carlosmmatos changed the title fix(security): harden OAuth and download redirects against credential replay fix(security): harden OAuth and fetch_tags redirects against credential replay Sep 8, 2026
Keep the redirect hardening; drop the multi-line explanatory blocks.
…the redirect

Dropping -L and setting -MaximumRedirection 0 stops a 307/308 from replaying the
client secret in the request body, but it also disabled region auto-discovery,
which is what that redirect is for.

Measured against the live API on curl 7.29.0 and 7.76.1: a wrong-region token
request answers 308 with x-cs-region and a Location, and following it with -L
does return a token, so the body is replayed.

bash now reads x-cs-region off the un-followed response and re-issues the
request against that region, resolved through cs_cloud(), which is a closed
allowlist that dies on anything it does not recognise. cs_cloud() takes the
region as an optional argument for that, defaulting to the current one, so every
existing call site is unchanged. The retry dumps headers to a separate file so
the existing region-hint block still reads the first response, and the payload
stays on stdin rather than in argv. cs_cloud() failing is checked rather than
assumed to end the script, because exiting a nested command substitution does
not stop the caller under bash.

Windows PowerShell 5.1 returns a 3xx rather than throwing it, so
ConvertFrom-Json received the redirect body as a Byte[] and the catch reported
an unhandled error. Invoke-FalconAuth now handles the redirect on the success
path as well as in the catch, and reads X-Cs-Region through a helper, because
5.1 gives a WebHeaderCollection with only a string indexer while PowerShell 7
gives HttpResponseHeaders, where that indexer returns empty.
@carlosmmatos-cs carlosmmatos-cs changed the title fix(security): harden OAuth and fetch_tags redirects against credential replay fix(security): harden OAuth and fetch_tags redirects, keep region auto-discovery Sep 8, 2026
fetch_tags lost -L, so --proto-redir has nothing to act on. Leaving it there
reads as though redirects were considered and handled on that call, which is
misleading in a change about redirects.

Measured with a pass-through curl wrapper over the real --list-tags path: the
registry token request and the tags list both answer 200 in a single hop, as do
the ccid and image-registry-credentials calls, so nothing on that path wants a
redirect. That was against registry.crowdstrike.com; the gov registries were not
reachable from the test environment.

The convention is now uniform across all four scripts: --proto-redir appears on
exactly the curl invocations that pass -L.
…ionHeader

The note claimed the success path yields "a Dictionary, so ContainsKey". That was
an assumption. Measured on Windows PowerShell 5.1.26100.9168: Invoke-WebRequest
returns a body-less 3xx as Microsoft.PowerShell.Commands.WebResponseObject, whose
Content is a Byte[] — which is precisely why the old code failed inside
ConvertFrom-Json before it could reach the region logic.

The note now distinguishes what was measured on each platform and path from what
rests on documentation, and no longer names a type nothing verified.

Comment only; no behaviour change. The functional test still passes on
PowerShell 7.6.5 and on 5.1.
@carlosmmatos-cs
carlosmmatos-cs merged commit dad81bf into CrowdStrike:main Sep 8, 2026
carlosmmatos-cs added a commit that referenced this pull request Sep 9, 2026
…s a redirect (#525)

* fix(security): stop curl_command from carrying the bearer token across a redirect

curl_command() passed -L, so a redirect was followed with the OAuth bearer
token still on curl's configuration input. curl fixed the cross-host case in
7.58.0 as CVE-2018-1000007, which leaves roughly 7.30 through 7.57 exposed.

Measured against the live API: every endpoint curl_command touches answers in a
single hop on the correct region, including download-installer/v3 and the
registry tags list. So -L never fired on a correct-region run.

-L only did work when FALCON_CLOUD named the wrong region, because the API
answers a wrong region with a 308 to the right one. That never worked on a curl
that strips the header, which is every supported version: on curl 7.29.0 and
8.5.0 the redirect was followed, the header was dropped, the call came back 401
and the run died with a misleading "No sensor found for OS" error. The only
versions where -L produced a working request are the same versions that leak the
token.

Dropped -L, and with it --proto-redir, keeping the convention from #521 that
--proto-redir appears only next to -L. The wrong-region case is now handled the
way the OAuth token request already handles it: the x-cs-region hint is adopted
instead of the redirect being followed, so it works on every curl version rather
than only the leaky band. The warning naming the real region still prints.
falcon-container-sensor-pull.sh already adopted the hint; install, uninstall and
migrate only warned and kept the wrong region.

Fixes #523

* fix(security): handle the region redirect in curl_command instead of following it

Dropping -L closed the leak but left the wrong-region case relying on
get_oauth_token having corrected cs_falcon_cloud first. That only covers the
client id and secret path: with FALCON_ACCESS_TOKEN there is no token POST, so
there is no x-cs-region to read, and every API call went to the wrong region.

curl_command now reads x-cs-region off the un-followed redirect and re-issues
against that region, resolved through cs_cloud(), so the retry host always comes
from a closed allowlist and never from Location. Region correction now covers
every request that carries the token, whichever way the token was obtained.

There is no scope-free way to discover this up front: the 308 only comes back on
a real routable path. An unknown path answers 404 with no x-cs-region, and the
redirect is emitted after authentication, so an unauthenticated probe gets 401.
The retry therefore rides on the caller's own request rather than a probe.

The body is buffered because the redirect body is 107 bytes, not empty, so
emitting it would corrupt the value the caller captures. Buffering is safe for
the -o callers too: curl writes their file itself and stdout stays empty.

The status is read from the last HTTP status line, because a proxy CONNECT dumps
one of its own first. The exit code is captured and returned so behaviour under
set -e is unchanged and #526's call-site guards still receive the real code -
measured: rc=5 for an unresolvable proxy, both under set +e and through a
command substitution.

Verified against the live API on curl 7.29.0 and 8.5.0, with client credentials
and with FALCON_ACCESS_TOKEN, for us-1, us-2 and eu-1: GET, GET with -o, PATCH
with a JSON body, and the query holding a literal pipe all reach the correct
region. The arg rewrite was checked separately under dash, bash and macOS sh.
carlosmmatos-cs added a commit that referenced this pull request Sep 10, 2026
Bumps the version string from 1.13.0 to 1.14.0 across all scripts and READMEs
ahead of the v1.14.0 release. This is a minor bump because of the new opt-in
FALCON_DEBUG mode added to the bash and PowerShell scripts (#522), which ships
alongside four fixes: the credential protections in the deployment scripts
(#520), the OAuth and fetch_tags redirect hardening that keeps region
auto-discovery (#521), the curl_command fix that stops the bearer token
crossing a redirect (#525), and the handle_curl_error path under sh (#526).

Updates the VERSION and $ScriptVersion constants in the bash and PowerShell
scripts, the Version usage lines in the READMEs, the pinned
raw.githubusercontent.com URLs, and the FALCON_DEBUG sample output.
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.

2 participants