Skip to content

Commit cd7a9c8

Browse files
fix(security): protect credentials in deployment scripts
1 parent 3fdfcee commit cd7a9c8

16 files changed

Lines changed: 640 additions & 120 deletions
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
6+
work_dir=$(mktemp -d)
7+
trap 'rm -rf "$work_dir"' EXIT
8+
9+
fail() {
10+
echo "FAIL: $*" >&2
11+
exit 1
12+
}
13+
14+
curl() {
15+
printf '%s\n' "$@" >"$CURL_ARGS_FILE"
16+
cat >"$CURL_STDIN_FILE"
17+
}
18+
19+
export CURL_ARGS_FILE CURL_STDIN_FILE
20+
21+
test_curl_helper() {
22+
local script=$1 token_mode=$2
23+
local helper
24+
25+
helper=$(awk '/^curl_command\(\)/,/^}/' "$repo_root/$script")
26+
[ -n "$helper" ] || fail "curl_command not found in $script"
27+
eval "$helper"
28+
29+
for old_curl in 0 1; do
30+
CURL_ARGS_FILE="$work_dir/args"
31+
CURL_STDIN_FILE="$work_dir/stdin"
32+
proxy=""
33+
export proxy
34+
cs_falcon_oauth_token="REGRESSION_SECRET_TOKEN"
35+
36+
if [ "$token_mode" = "argument" ]; then
37+
curl_command "$cs_falcon_oauth_token" "https://api.example.invalid/resource"
38+
else
39+
curl_command "https://api.example.invalid/resource"
40+
fi
41+
42+
if grep -qF "$cs_falcon_oauth_token" "$CURL_ARGS_FILE"; then
43+
fail "$script exposed the bearer token in curl arguments (old_curl=$old_curl)"
44+
fi
45+
grep -qF 'https://api.example.invalid/resource' "$CURL_ARGS_FILE" ||
46+
fail "$script did not pass the expected URL (old_curl=$old_curl)"
47+
grep -qF "$cs_falcon_oauth_token" "$CURL_STDIN_FILE" ||
48+
fail "$script did not provide the bearer token through stdin (old_curl=$old_curl)"
49+
grep -qF -- '--proto' "$CURL_ARGS_FILE" ||
50+
fail "$script did not restrict the request protocol (old_curl=$old_curl)"
51+
grep -qF -- '--proto-redir' "$CURL_ARGS_FILE" ||
52+
fail "$script did not restrict the redirect protocol (old_curl=$old_curl)"
53+
done
54+
}
55+
56+
test_curl_helper \
57+
bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh argument
58+
test_curl_helper bash/install/falcon-linux-install.sh global
59+
test_curl_helper bash/install/falcon-linux-uninstall.sh global
60+
test_curl_helper bash/migrate/falcon-linux-migrate.sh global
61+
62+
test_xtrace_guard() {
63+
local script=$1 guard trace_file
64+
65+
guard=$(awk '/^case \$- in$/,/^esac$/' "$repo_root/$script")
66+
[ -n "$guard" ] || fail "xtrace guard not found in $script"
67+
trace_file="$work_dir/xtrace"
68+
69+
FALCON_CLIENT_SECRET="XTRACE_SECRET_SENTINEL" \
70+
bash -xc "$guard; : \"\$FALCON_CLIENT_SECRET\"" \
71+
>/dev/null 2>"$trace_file"
72+
73+
if grep -qF 'XTRACE_SECRET_SENTINEL' "$trace_file"; then
74+
fail "$script allowed a credential into bash xtrace output"
75+
fi
76+
}
77+
78+
test_xtrace_guard bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh
79+
test_xtrace_guard bash/install/falcon-linux-install.sh
80+
test_xtrace_guard bash/install/falcon-linux-uninstall.sh
81+
test_xtrace_guard bash/migrate/falcon-linux-migrate.sh
82+
83+
test_hash_verification() {
84+
local script=$1 helper test_file expected_sha
85+
86+
helper=$(awk '/^verify_sha256\(\)/,/^}/' "$repo_root/$script")
87+
[ -n "$helper" ] || fail "verify_sha256 not found in $script"
88+
eval "$helper"
89+
die() { exit 1; }
90+
91+
test_file="$work_dir/installer"
92+
printf '%s' 'verified installer content' >"$test_file"
93+
expected_sha=$(openssl dgst -sha256 "$test_file" | awk '{ print $NF }')
94+
verify_sha256 "$test_file" "$expected_sha" ||
95+
fail "$script rejected a valid installer hash"
96+
97+
if (verify_sha256 "$test_file" '0000000000000000000000000000000000000000000000000000000000000000'); then
98+
fail "$script accepted an invalid installer hash"
99+
fi
100+
[ ! -e "$test_file" ] || fail "$script retained an installer with an invalid hash"
101+
}
102+
103+
test_hash_verification bash/install/falcon-linux-install.sh
104+
test_hash_verification bash/migrate/falcon-linux-migrate.sh
105+
106+
if rg -n 'Invalid Access Token:.*\$cs_falcon_oauth_token|Failed to retrieve maintenance token\. Response:' \
107+
"$repo_root/bash" --glob '*.sh'; then
108+
fail 'a Bash error path exposes a credential or raw maintenance-token response'
109+
fi
110+
111+
if rg -n 'curl .*X-aws-ec2-metadata-token:.*\$token' \
112+
"$repo_root/bash" --glob '*.sh'; then
113+
fail 'an EC2 metadata token is exposed in curl arguments'
114+
fi
115+
116+
if rg -n '(Invoke-FalconAuth|GetToken) - \$content:|Retrieved maintenance token:|Starting .*parameters.*\$(Install|Uninstall)Params' \
117+
"$repo_root/powershell" --glob '*.ps1'; then
118+
fail 'a PowerShell log statement exposes an authentication or installer token'
119+
fi
120+
121+
echo 'PASS: credential handling regression checks'
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/bin/sh
2+
3+
set -eu
4+
5+
work_dir=$(mktemp -d)
6+
server_pids=""
7+
cleanup() {
8+
for pid in $server_pids; do
9+
kill "$pid" 2>/dev/null || true
10+
done
11+
rm -rf "$work_dir"
12+
}
13+
trap cleanup EXIT
14+
15+
start_capture_server() {
16+
port=$1 output=$2
17+
python3 -c 'import http.server, socketserver, sys
18+
socketserver.TCPServer.allow_reuse_address = True
19+
class Handler(http.server.BaseHTTPRequestHandler):
20+
def do_GET(self):
21+
with open(sys.argv[1], "w") as output:
22+
output.write(self.headers.get("Authorization", ""))
23+
self.send_response(204)
24+
self.end_headers()
25+
def log_message(self, *args):
26+
pass
27+
http.server.HTTPServer(("127.0.0.1", int(sys.argv[2])), Handler).handle_request()
28+
' "$output" "$port" &
29+
last_server_pid=$!
30+
server_pids="$server_pids $last_server_pid"
31+
}
32+
33+
token=CONTAINER_LIVE_TEST_TOKEN
34+
curl --version | head -n 1
35+
36+
start_capture_server 28768 "$work_dir/header"
37+
header_pid=$last_server_pid
38+
sleep 1
39+
printf 'oauth2-bearer = "%s"\n' "$token" |
40+
curl --silent --show-error -K- --url http://127.0.0.1:28768/test
41+
wait "$header_pid"
42+
[ "$(cat "$work_dir/header")" = "Bearer $token" ] || {
43+
echo 'FAIL: OAuth configuration did not transmit the expected header' >&2
44+
exit 1
45+
}
46+
47+
start_capture_server 28770 "$work_dir/redirect-header"
48+
target_pid=$last_server_pid
49+
python3 -c 'import http.server, socketserver
50+
socketserver.TCPServer.allow_reuse_address = True
51+
class Handler(http.server.BaseHTTPRequestHandler):
52+
def do_GET(self):
53+
self.send_response(302)
54+
self.send_header("Location", "http://localhost:28770/target")
55+
self.end_headers()
56+
def log_message(self, *args):
57+
pass
58+
http.server.HTTPServer(("127.0.0.1", 28769), Handler).handle_request()
59+
' &
60+
redirect_pid=$!
61+
server_pids="$server_pids $redirect_pid"
62+
sleep 1
63+
printf 'oauth2-bearer = "%s"\n' "$token" |
64+
curl --silent --show-error -L -K- --url http://127.0.0.1:28769/start
65+
wait "$redirect_pid" "$target_pid"
66+
[ ! -s "$work_dir/redirect-header" ] || {
67+
echo 'FAIL: OAuth credential followed a cross-host redirect' >&2
68+
exit 1
69+
}
70+
71+
echo 'PASS: OAuth stdin configuration and cross-host redirect protection'
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
$ErrorActionPreference = 'Stop'
2+
3+
$RepositoryRoot = Resolve-Path (Join-Path $PSScriptRoot '../..')
4+
$Scripts = @(
5+
'powershell/install/falcon_windows_install.ps1'
6+
'powershell/install/falcon_windows_uninstall.ps1'
7+
'powershell/migrate/falcon_windows_migrate.ps1'
8+
)
9+
$SensitiveLogVariables = '(FalconAccessToken|FalconClientSecret|MaintenanceToken|ProvToken|InstallParams|UninstallParams)'
10+
$Failures = [System.Collections.Generic.List[string]]::new()
11+
12+
foreach ($RelativePath in $Scripts) {
13+
$Path = Join-Path $RepositoryRoot $RelativePath
14+
$Tokens = $null
15+
$ParseErrors = $null
16+
$Ast = [System.Management.Automation.Language.Parser]::ParseFile(
17+
$Path,
18+
[ref] $Tokens,
19+
[ref] $ParseErrors
20+
)
21+
22+
foreach ($ParseError in $ParseErrors) {
23+
$Failures.Add("${RelativePath}:$($ParseError.Extent.StartLineNumber): parser error: $($ParseError.Message)")
24+
}
25+
26+
$LogCommands = $Ast.FindAll({
27+
param($Node)
28+
$Node -is [System.Management.Automation.Language.CommandAst] -and
29+
$Node.GetCommandName() -in @('Write-FalconLog', 'Write-VerboseLog')
30+
}, $true)
31+
32+
foreach ($Command in $LogCommands) {
33+
$CommandText = $Command.Extent.Text
34+
if ($CommandText -match "\`$$SensitiveLogVariables") {
35+
$Failures.Add("${RelativePath}:$($Command.Extent.StartLineNumber): sensitive variable used in log command")
36+
}
37+
if ($CommandText -match 'Write-VerboseLog' -and
38+
$CommandText -match '\$content' -and
39+
$CommandText -match '(Invoke-FalconAuth|GetToken)') {
40+
$Failures.Add("${RelativePath}:$($Command.Extent.StartLineNumber): sensitive API response used in verbose log")
41+
}
42+
}
43+
44+
$DebugOffCommands = $Ast.FindAll({
45+
param($Node)
46+
$Node -is [System.Management.Automation.Language.CommandAst] -and
47+
$Node.GetCommandName() -eq 'Set-PSDebug' -and
48+
$Node.Extent.Text -match '-Off'
49+
}, $true)
50+
if ($DebugOffCommands.Count -eq 0) {
51+
$Failures.Add("${RelativePath}: PowerShell tracing is not disabled before credentials are processed")
52+
}
53+
}
54+
55+
if ($Failures.Count -gt 0) {
56+
$Failures | ForEach-Object { Write-Error $_ }
57+
exit 1
58+
}
59+
60+
Write-Output 'PASS: PowerShell credential logging and parser checks'

.github/workflows/container_sensor_pull.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ jobs:
3131
shellcheck --version
3232
shellcheck bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh
3333
34+
- name: Unit tests
35+
run: sh bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh
36+
3437
container-test:
3538
name: Container Test
3639
needs: validate
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: "CI: credential handling"
2+
3+
on:
4+
push:
5+
paths:
6+
- '.github/scripts/test-credential-handling.sh'
7+
- '.github/scripts/test-curl-oauth-live.sh'
8+
- '.github/scripts/test-powershell-credential-handling.ps1'
9+
- '.github/workflows/credential_handling.yml'
10+
- 'bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh'
11+
- 'bash/install/falcon-linux-install.sh'
12+
- 'bash/install/falcon-linux-uninstall.sh'
13+
- 'bash/migrate/falcon-linux-migrate.sh'
14+
- 'powershell/install/falcon_windows_install.ps1'
15+
- 'powershell/install/falcon_windows_uninstall.ps1'
16+
- 'powershell/migrate/falcon_windows_migrate.ps1'
17+
pull_request:
18+
paths:
19+
- '.github/scripts/test-credential-handling.sh'
20+
- '.github/scripts/test-curl-oauth-live.sh'
21+
- '.github/scripts/test-powershell-credential-handling.ps1'
22+
- '.github/workflows/credential_handling.yml'
23+
- 'bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh'
24+
- 'bash/install/falcon-linux-install.sh'
25+
- 'bash/install/falcon-linux-uninstall.sh'
26+
- 'bash/migrate/falcon-linux-migrate.sh'
27+
- 'powershell/install/falcon_windows_install.ps1'
28+
- 'powershell/install/falcon_windows_uninstall.ps1'
29+
- 'powershell/migrate/falcon_windows_migrate.ps1'
30+
31+
permissions:
32+
contents: read
33+
34+
jobs:
35+
test:
36+
name: Prevent credential exposure
37+
runs-on: ubuntu-latest
38+
steps:
39+
- name: Check out code
40+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
41+
with:
42+
persist-credentials: false
43+
44+
- name: Run credential handling regression tests
45+
run: bash .github/scripts/test-credential-handling.sh
46+
47+
- name: Run PowerShell credential handling regression tests
48+
shell: pwsh
49+
run: ./.github/scripts/test-powershell-credential-handling.ps1
50+
51+
- name: Test curl OAuth behavior
52+
run: sh .github/scripts/test-curl-oauth-live.sh
53+
54+
curl-container-compatibility:
55+
name: curl compatibility (${{ matrix.name }})
56+
runs-on: ubuntu-latest
57+
strategy:
58+
fail-fast: false
59+
matrix:
60+
include:
61+
- name: Ubuntu 22.04
62+
image: ubuntu:22.04
63+
setup: apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl python3 >/dev/null
64+
- name: Debian 12
65+
image: debian:12-slim
66+
setup: apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl python3 >/dev/null
67+
- name: Alpine 3.20
68+
image: alpine:3.20
69+
setup: apk add --no-cache curl python3 >/dev/null
70+
- name: AlmaLinux 9
71+
image: almalinux:9
72+
setup: dnf install -y -q python3 >/dev/null
73+
- name: Amazon Linux 2023
74+
image: amazonlinux:2023
75+
setup: dnf install -y -q python3 >/dev/null
76+
steps:
77+
- name: Check out code
78+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
79+
with:
80+
persist-credentials: false
81+
82+
- name: Test packaged curl
83+
env:
84+
CONTAINER_IMAGE: ${{ matrix.image }}
85+
CONTAINER_SETUP: ${{ matrix.setup }}
86+
run: |
87+
docker run --rm \
88+
--volume "$PWD:/repo:ro" \
89+
--env CONTAINER_SETUP \
90+
"$CONTAINER_IMAGE" \
91+
sh -c '$CONTAINER_SETUP && sh /repo/.github/scripts/test-curl-oauth-live.sh'

bash/containers/falcon-container-sensor-pull/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ CrowdStrike now provides unified images that work across all regions:
5252

5353
### Use cURL version 7.55.0 or later
5454

55-
We've identified a security concern related to cURL versions 7.54.1 and earlier. In these versions, request headers were set using the `-H` option, which allowed potential secrets to be exposed via the command line. In newer versions of cURL, versions 7.55.0 and later, you can pass headers from stdin using the `@-` syntax, which addresses this security concern. **We recommend that you to upgrade cURL to version 7.55.0 or later**. If this is not possible, this script offers compatibility with the older method through the use of the `--allow-legacy-curl` optional command line flag.
55+
OAuth credentials are supplied to cURL through its configuration input rather than command-line arguments. This also lets cURL treat the bearer token as an authentication credential and prevents it from forwarding the token when a redirect crosses to another host. **We recommend upgrading cURL to version 7.55.0 or later.** If this is not possible, `--allow-legacy-curl` bypasses the version check without reverting to command-line credential handling.
5656

5757
To check your version of cURL, run the following command: `curl --version`
5858

0 commit comments

Comments
 (0)