Skip to content

Commit dad81bf

Browse files
fix(security): harden OAuth and fetch_tags redirects, keep region auto-discovery (#521)
* fix(security): harden OAuth and download redirects against credential 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 #520. Live-validated on the fork (CAND-001/002/003/004). * fix(security): drop download rewrite; keep oauth and fetch_tags redirect hardening Co-authored-by: Carlos Matos <carlosmmatos@users.noreply.github.com> * chore: strip verbose oauth and fetch_tags comments Keep the redirect hardening; drop the multi-line explanatory blocks. * fix(security): re-issue the OAuth token request instead of following 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. * fix(container-pull): drop the dead redirect pin from fetch_tags 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. * docs(powershell): correct the header-collection note in Get-FalconRegionHeader 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. * chore: trim verbose comments in oauth redirect hardening --------- Co-authored-by: Carlos Matos <carlosmmatos@users.noreply.github.com> Co-authored-by: Carlos Matos <carlos.matos@crowdstrike.com>
1 parent 205b919 commit dad81bf

7 files changed

Lines changed: 365 additions & 117 deletions

File tree

bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,16 @@ deprecated() {
9292
}
9393

9494
cs_cloud() {
95-
case "${FALCON_CLOUD}" in
95+
# $1 optionally overrides FALCON_CLOUD, used by the OAuth region retry.
96+
local region="${1:-$FALCON_CLOUD}"
97+
case "${region}" in
9698
us-1) echo "api.crowdstrike.com" ;;
9799
us-2) echo "api.us-2.crowdstrike.com" ;;
98100
us-3) echo "api.us-3.crowdstrike.com" ;;
99101
eu-1) echo "api.eu-1.crowdstrike.com" ;;
100102
us-gov-1) echo "api.laggar.gcw.crowdstrike.com" ;;
101103
us-gov-2) echo "api.us-gov-2.crowdstrike.mil" ;;
102-
*) die "Unrecognized region option: ${FALCON_CLOUD}" ;;
104+
*) die "Unrecognized region option: ${region}" ;;
103105
esac
104106
}
105107

@@ -294,8 +296,10 @@ curl_command() {
294296
}
295297

296298
fetch_tags() {
299+
# No -L, so --proto-redir is dropped too; nothing follows a redirect here.
297300
bearer_result=$(echo "-u $ART_USERNAME:$ART_PASSWORD" |
298-
curl -s -L "https://$cs_registry/v2/token?account=$ART_USERNAME&scope=repository:$registry_opts/$repository_name:pull&service=$cs_registry" -K-)
301+
curl -s --proto '=https' \
302+
"https://$cs_registry/v2/token?account=$ART_USERNAME&scope=repository:$registry_opts/$repository_name:pull&service=$cs_registry" -K-)
299303
handle_curl_error $?
300304
registry_bearer=$(echo "$bearer_result" | json_value "token" | sed 's/ *$//g' | sed 's/^ *//g')
301305
# Check if registry_bearer is not empty
@@ -676,20 +680,45 @@ VARIABLES="FALCON_CLIENT_ID FALCON_CLIENT_SECRET"
676680
[ -n "$VAR_UNSET" ] && usage
677681
}
678682

683+
# POSTs the OAuth payload from stdin, never argv. $1 = API host, $2 = header dump path.
684+
oauth_token_request() {
685+
curl -X POST -s --proto '=https' "https://$1/oauth2/token" \
686+
-H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
687+
-H "User-Agent: crowdstrike-falcon-script/$VERSION" \
688+
--dump-header "$2" \
689+
--data @-
690+
}
691+
679692
response_headers=$(mktemp)
680693
cs_falcon_oauth_token=$(
681694
if ! command -v curl >/dev/null 2>&1; then
682695
die "The 'curl' command is missing. Please install it before continuing. Aborting..."
683696
fi
684697
685-
token_result=$(echo "client_id=$FALCON_CLIENT_ID&client_secret=$FALCON_CLIENT_SECRET" |
686-
curl -X POST -s -L "https://$(cs_cloud)/oauth2/token" \
687-
-H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
688-
-H "User-Agent: crowdstrike-falcon-script/$VERSION" \
689-
--dump-header "$response_headers" \
690-
--data @-)
698+
auth_payload="client_id=$FALCON_CLIENT_ID&client_secret=$FALCON_CLIENT_SECRET"
699+
700+
token_result=$(echo "$auth_payload" | oauth_token_request "$(cs_cloud)" "$response_headers")
691701
handle_curl_error $?
692702
token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g')
703+
if [ -z "$token" ]; then
704+
# Wrong region: retry against the x-cs-region hint instead of following
705+
# the redirect, which would replay the secret to Location.
706+
hinted=$(grep -i ^x-cs-region: "$response_headers" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g')
707+
if [ -n "$hinted" ] && [ "$hinted" != "$FALCON_CLOUD" ]; then
708+
# cs_cloud() validates the hint against its own allowlist. Check for
709+
# empty rather than trusting its die, which does not stop bash.
710+
retry_host=$(cs_cloud "$hinted")
711+
if [ -n "$retry_host" ]; then
712+
# Separate file: --dump-header truncates, and region_hint below
713+
# still needs the original response.
714+
retry_headers=$(mktemp)
715+
token_result=$(echo "$auth_payload" | oauth_token_request "$retry_host" "$retry_headers")
716+
handle_curl_error $?
717+
rm -f "$retry_headers"
718+
token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g')
719+
fi
720+
fi
721+
fi
693722
if [ -z "$token" ]; then
694723
die "Unable to obtain CrowdStrike Falcon OAuth Token. Double check your credentials and/or ensure you set the correct cloud region."
695724
fi

bash/install/falcon-linux-install.sh

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -648,14 +648,16 @@ die() {
648648
}
649649

650650
cs_cloud() {
651-
case "${cs_falcon_cloud}" in
651+
# $1 optionally overrides cs_falcon_cloud, used by the OAuth region retry.
652+
local region="${1:-$cs_falcon_cloud}"
653+
case "${region}" in
652654
us-1) echo "api.crowdstrike.com" ;;
653655
us-2) echo "api.us-2.crowdstrike.com" ;;
654656
us-3) echo "api.us-3.crowdstrike.com" ;;
655657
eu-1) echo "api.eu-1.crowdstrike.com" ;;
656658
us-gov-1) echo "api.laggar.gcw.crowdstrike.com" ;;
657659
us-gov-2) echo "api.us-gov-2.crowdstrike.mil" ;;
658-
*) die "Unrecognized Falcon Cloud: ${cs_falcon_cloud}" ;;
660+
*) die "Unrecognized Falcon Cloud: ${region}" ;;
659661
esac
660662
}
661663

@@ -769,6 +771,15 @@ get_user_agent() {
769771
echo "$user_agent"
770772
}
771773

774+
# POSTs the OAuth payload from stdin, never argv. $1 = API host, $2 = header dump path.
775+
oauth_token_request() {
776+
curl -X POST -s -x "$proxy" --proto '=https' "https://$1/oauth2/token" \
777+
-H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
778+
-H "User-Agent: $(get_user_agent)" \
779+
--dump-header "$2" \
780+
--data @-
781+
}
782+
772783
get_oauth_token() {
773784
# Get credentials first
774785
get_falcon_credentials
@@ -777,16 +788,32 @@ get_oauth_token() {
777788
if [ -n "$FALCON_ACCESS_TOKEN" ]; then
778789
token=$FALCON_ACCESS_TOKEN
779790
else
780-
token_result=$(echo "client_id=$cs_falcon_client_id&client_secret=$cs_falcon_client_secret" |
781-
curl -X POST -s -x "$proxy" -L "https://$(cs_cloud)/oauth2/token" \
782-
-H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
783-
-H "User-Agent: $(get_user_agent)" \
784-
--dump-header "${response_headers}" \
785-
--data @-)
791+
auth_payload="client_id=$cs_falcon_client_id&client_secret=$cs_falcon_client_secret"
792+
793+
token_result=$(echo "$auth_payload" | oauth_token_request "$(cs_cloud)" "${response_headers}")
786794
787795
handle_curl_error $?
788796
789797
token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g')
798+
if [ -z "$token" ]; then
799+
# Wrong region: retry against the x-cs-region hint instead of
800+
# following the redirect, which would replay the secret to Location.
801+
hinted=$(grep -i ^x-cs-region: "${response_headers}" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g')
802+
if [ -n "$hinted" ] && [ "$hinted" != "$cs_falcon_cloud" ]; then
803+
# cs_cloud() validates the hint against its own allowlist. Check
804+
# for empty rather than trusting its die, which does not stop bash.
805+
retry_host=$(cs_cloud "$hinted")
806+
if [ -n "$retry_host" ]; then
807+
# Separate file: --dump-header truncates, and region_hint below
808+
# still needs the original response.
809+
retry_headers=$(mktemp)
810+
token_result=$(echo "$auth_payload" | oauth_token_request "$retry_host" "$retry_headers")
811+
handle_curl_error $?
812+
rm -f "$retry_headers"
813+
token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g')
814+
fi
815+
fi
816+
fi
790817
if [ -z "$token" ]; then
791818
die "Unable to obtain CrowdStrike Falcon OAuth Token. Double check your credentials and/or ensure you set the correct cloud region."
792819
fi

bash/install/falcon-linux-uninstall.sh

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -201,14 +201,16 @@ cs_remove_host_from_console() {
201201
}
202202

203203
cs_cloud() {
204-
case "${cs_falcon_cloud}" in
204+
# $1 optionally overrides cs_falcon_cloud, used by the OAuth region retry.
205+
local region="${1:-$cs_falcon_cloud}"
206+
case "${region}" in
205207
us-1) echo "api.crowdstrike.com" ;;
206208
us-2) echo "api.us-2.crowdstrike.com" ;;
207209
us-3) echo "api.us-3.crowdstrike.com" ;;
208210
eu-1) echo "api.eu-1.crowdstrike.com" ;;
209211
us-gov-1) echo "api.laggar.gcw.crowdstrike.com" ;;
210212
us-gov-2) echo "api.us-gov-2.crowdstrike.mil" ;;
211-
*) die "Unrecognized Falcon Cloud: ${cs_falcon_cloud}" ;;
213+
*) die "Unrecognized Falcon Cloud: ${region}" ;;
212214
esac
213215
}
214216

@@ -430,6 +432,15 @@ get_user_agent() {
430432
echo "$user_agent"
431433
}
432434

435+
# POSTs the OAuth payload from stdin, never argv. $1 = API host, $2 = header dump path.
436+
oauth_token_request() {
437+
curl -X POST -s -x "$proxy" --proto '=https' "https://$1/oauth2/token" \
438+
-H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
439+
-H "User-Agent: $(get_user_agent)" \
440+
--dump-header "$2" \
441+
--data @-
442+
}
443+
433444
get_oauth_token() {
434445
# Get credentials first
435446
get_falcon_credentials
@@ -438,16 +449,32 @@ get_oauth_token() {
438449
if [ -n "$FALCON_ACCESS_TOKEN" ]; then
439450
token=$FALCON_ACCESS_TOKEN
440451
else
441-
token_result=$(echo "client_id=$cs_falcon_client_id&client_secret=$cs_falcon_client_secret" |
442-
curl -X POST -s -x "$proxy" -L "https://$(cs_cloud)/oauth2/token" \
443-
-H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
444-
-H "User-Agent: $(get_user_agent)" \
445-
--dump-header "${response_headers}" \
446-
--data @-)
452+
auth_payload="client_id=$cs_falcon_client_id&client_secret=$cs_falcon_client_secret"
453+
454+
token_result=$(echo "$auth_payload" | oauth_token_request "$(cs_cloud)" "${response_headers}")
447455
448456
handle_curl_error $?
449457
450458
token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g')
459+
if [ -z "$token" ]; then
460+
# Wrong region: retry against the x-cs-region hint instead of
461+
# following the redirect, which would replay the secret to Location.
462+
hinted=$(grep -i ^x-cs-region: "${response_headers}" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g')
463+
if [ -n "$hinted" ] && [ "$hinted" != "$cs_falcon_cloud" ]; then
464+
# cs_cloud() validates the hint against its own allowlist. Check
465+
# for empty rather than trusting its die, which does not stop bash.
466+
retry_host=$(cs_cloud "$hinted")
467+
if [ -n "$retry_host" ]; then
468+
# Separate file: --dump-header truncates, and region_hint below
469+
# still needs the original response.
470+
retry_headers=$(mktemp)
471+
token_result=$(echo "$auth_payload" | oauth_token_request "$retry_host" "$retry_headers")
472+
handle_curl_error $?
473+
rm -f "$retry_headers"
474+
token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g')
475+
fi
476+
fi
477+
fi
451478
if [ -z "$token" ]; then
452479
die "Unable to obtain CrowdStrike Falcon OAuth Token. Double check your credentials and/or ensure you set the correct cloud region."
453480
fi

bash/migrate/falcon-linux-migrate.sh

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,16 @@ check_package_manager_lock() {
291291
}
292292

293293
cs_cloud() {
294-
case "${cs_falcon_cloud}" in
294+
# $1 optionally overrides cs_falcon_cloud, used by the OAuth region retry.
295+
local region="${1:-$cs_falcon_cloud}"
296+
case "${region}" in
295297
us-1) echo "api.crowdstrike.com" ;;
296298
us-2) echo "api.us-2.crowdstrike.com" ;;
297299
us-3) echo "api.us-3.crowdstrike.com" ;;
298300
eu-1) echo "api.eu-1.crowdstrike.com" ;;
299301
us-gov-1) echo "api.laggar.gcw.crowdstrike.com" ;;
300302
us-gov-2) echo "api.us-gov-2.crowdstrike.mil" ;;
301-
*) die "Unrecognized Falcon Cloud: ${cs_falcon_cloud}" ;;
303+
*) die "Unrecognized Falcon Cloud: ${region}" ;;
302304
esac
303305
}
304306

@@ -342,6 +344,15 @@ get_user_agent() {
342344
echo "$user_agent"
343345
}
344346

347+
# POSTs the OAuth payload from stdin, never argv. $1 = API host, $2 = header dump path.
348+
oauth_token_request() {
349+
curl -X POST -s -x "$proxy" --proto '=https' "https://$1/oauth2/token" \
350+
-H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
351+
-H "User-Agent: $(get_user_agent)" \
352+
--dump-header "$2" \
353+
--data @-
354+
}
355+
345356
get_oauth_token() {
346357
# Get credentials first
347358
get_falcon_credentials
@@ -359,16 +370,30 @@ get_oauth_token() {
359370
auth_payload="${auth_payload}&member_cid=${cs_falcon_member_cid}"
360371
fi
361372
362-
token_result=$(echo "$auth_payload" |
363-
curl -X POST -s -x "$proxy" -L "https://$(cs_cloud)/oauth2/token" \
364-
-H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
365-
-H "User-Agent: $(get_user_agent)" \
366-
--dump-header "${response_headers}" \
367-
--data @-)
373+
token_result=$(echo "$auth_payload" | oauth_token_request "$(cs_cloud)" "${response_headers}")
368374
369375
handle_curl_error $?
370376
371377
token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g')
378+
if [ -z "$token" ]; then
379+
# Wrong region: retry against the x-cs-region hint instead of
380+
# following the redirect, which would replay the secret to Location.
381+
hinted=$(grep -i ^x-cs-region: "${response_headers}" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g')
382+
if [ -n "$hinted" ] && [ "$hinted" != "$cs_falcon_cloud" ]; then
383+
# cs_cloud() validates the hint against its own allowlist. Check
384+
# for empty rather than trusting its die, which does not stop bash.
385+
retry_host=$(cs_cloud "$hinted")
386+
if [ -n "$retry_host" ]; then
387+
# Separate file: --dump-header truncates, and region_hint below
388+
# still needs the original response.
389+
retry_headers=$(mktemp)
390+
token_result=$(echo "$auth_payload" | oauth_token_request "$retry_host" "$retry_headers")
391+
handle_curl_error $?
392+
rm -f "$retry_headers"
393+
token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g')
394+
fi
395+
fi
396+
fi
372397
if [ -z "$token" ]; then
373398
die "Unable to obtain CrowdStrike Falcon OAuth Token. Double check your credentials and/or ensure you set the correct cloud region."
374399
fi

0 commit comments

Comments
 (0)