Skip to content

Commit 76643fa

Browse files
authored
fix(actions): preserve credentials with $ in setup action env vars (#532)
The setup action's 'Set environment variables' step wrote each credential to $GITHUB_ENV with an inline-interpolated echo (echo "VAR=${{ inputs.* }}"). GitHub substitutes the expression into the script text before bash runs, so bash then re-expanded any $WORD/$$/$(...) in the resulting double-quoted string — silently corrupting any credential containing '$' (e.g. an auto-generated WebDAV access key like abc$FOO123) and surfacing downstream as an unexplained 401 on the WebDAV PUT. The guards had the same flaw plus a shell-injection vector. All inputs now flow through the step's env: block (IN_*) and are written via a set_env() helper using GitHub's heredoc env syntax (NAME<<DELIM ... DELIM) with a per-run random delimiter — the same mechanism @actions/core uses. Values are emitted with printf as arguments, so $, %, quotes, backticks, $(...), = and embedded newlines are all preserved byte-for-byte with no shell, printf, or $GITHUB_ENV-format reinterpretation. selfsigned keeps its true/1 guard. Adds a setup-special-char-credentials regression job to test-actions.yml that round-trips $-bearing credentials (including a renamed input, short-code -> SFCC_SHORTCODE) and asserts byte-for-byte preservation. W-23178876
1 parent 78eaea0 commit 76643fa

3 files changed

Lines changed: 133 additions & 49 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@salesforce/b2c-dx-docs': patch
3+
---
4+
5+
Fix the `setup` GitHub Action silently corrupting any credential that contains a `$` (for example an auto-generated WebDAV access key like `abc$FOO123`). The action wrote credentials to the job environment with an inline-interpolated `echo`, so bash re-expanded `$WORD` sequences and stripped them — the altered credential then failed downstream WebDAV auth with an unexplained 401. Credentials are now passed through the step's `env` block and written with GitHub's heredoc env syntax, so values containing `$`, quotes, backticks, `$(...)`, `=`, or even newlines reach the CLI byte-for-byte. No workflow changes are required; re-run with the fixed action version.

.github/workflows/test-actions.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,65 @@ jobs:
4545
[ "$NO_COLOR" = "1" ] || (echo "NO_COLOR not set" && exit 1)
4646
echo "All environment variables verified"
4747
48+
# Regression test for W-23178876: credentials containing shell-significant
49+
# sequences ($WORD, $$, $(...), backticks, quotes) must reach $GITHUB_ENV
50+
# byte-for-byte. The old `echo "VAR=${{ inputs.* }}"` pattern let bash re-expand
51+
# those sequences, silently corrupting credentials (e.g. a WebDAV access key
52+
# with `$`) and surfacing later as an unexplained 401.
53+
setup-special-char-credentials:
54+
name: 'Setup (credentials with $ / special chars)'
55+
runs-on: ubuntu-latest
56+
steps:
57+
- uses: actions/checkout@v6
58+
59+
- name: Run setup action with shell-significant credentials
60+
uses: ./actions/setup
61+
with:
62+
# All single-quoted YAML scalars → passed to the action verbatim.
63+
# short-code and tenant-id are included so the renamed-input mapping
64+
# (short-code → SFCC_SHORTCODE) is exercised, not just same-name inputs.
65+
client-id: 'client$ID123'
66+
client-secret: 'sec$(echo INJECTED)ret'
67+
username: 'user$HOME_name'
68+
password: 'pa$$key$FOO123$end'
69+
certificate-passphrase: 'pass`id`phrase'
70+
short-code: 'short$CODE_xyz'
71+
tenant-id: 'bbsv_$PRD123'
72+
73+
- name: Verify credentials round-tripped intact
74+
# Expected values are injected through this step's env block (literal, not
75+
# shell-evaluated), so the assertions themselves are immune to the very
76+
# expansion bug they guard against. Each EXPECTED_* must match the `with:`
77+
# value above exactly.
78+
env:
79+
EXPECTED_CLIENT_ID: 'client$ID123'
80+
EXPECTED_CLIENT_SECRET: 'sec$(echo INJECTED)ret'
81+
EXPECTED_USERNAME: 'user$HOME_name'
82+
EXPECTED_PASSWORD: 'pa$$key$FOO123$end'
83+
EXPECTED_CERTIFICATE_PASSPHRASE: 'pass`id`phrase'
84+
EXPECTED_SHORTCODE: 'short$CODE_xyz'
85+
EXPECTED_TENANT_ID: 'bbsv_$PRD123'
86+
run: |
87+
fail=0
88+
check() {
89+
# $1 = env var name, $2 = actual value, $3 = expected value
90+
if [ "$2" != "$3" ]; then
91+
printf 'MISMATCH %s: got [%s] expected [%s]\n' "$1" "$2" "$3"
92+
fail=1
93+
else
94+
printf 'OK %s\n' "$1"
95+
fi
96+
}
97+
check SFCC_CLIENT_ID "$SFCC_CLIENT_ID" "$EXPECTED_CLIENT_ID"
98+
check SFCC_CLIENT_SECRET "$SFCC_CLIENT_SECRET" "$EXPECTED_CLIENT_SECRET"
99+
check SFCC_USERNAME "$SFCC_USERNAME" "$EXPECTED_USERNAME"
100+
check SFCC_PASSWORD "$SFCC_PASSWORD" "$EXPECTED_PASSWORD"
101+
check SFCC_CERTIFICATE_PASSPHRASE "$SFCC_CERTIFICATE_PASSPHRASE" "$EXPECTED_CERTIFICATE_PASSPHRASE"
102+
check SFCC_SHORTCODE "$SFCC_SHORTCODE" "$EXPECTED_SHORTCODE"
103+
check SFCC_TENANT_ID "$SFCC_TENANT_ID" "$EXPECTED_TENANT_ID"
104+
[ "$fail" -eq 0 ] || { echo "Credential corruption detected"; exit 1; }
105+
echo "All credentials preserved verbatim"
106+
48107
setup-nightly:
49108
name: 'Setup (nightly)'
50109
runs-on: ubuntu-latest

actions/setup/action.yml

Lines changed: 69 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -118,66 +118,86 @@ runs:
118118
119119
- name: Set environment variables
120120
shell: bash
121+
# Inputs are forwarded through this step's env block — NOT interpolated
122+
# directly into the script body below. GitHub injects env values into the
123+
# process verbatim, so a credential is never re-parsed by the shell. If we
124+
# inlined `${{ inputs.password }}` into a double-quoted `echo`, the runner
125+
# would expand the expression into the literal credential text first, then
126+
# bash would perform its OWN expansion on that text: any `$WORD` (e.g. in a
127+
# WebDAV access key like `abc$FOO123`) would be stripped to empty, silently
128+
# corrupting the credential and surfacing later as an unexplained 401. The
129+
# same inlining would also let metacharacters (`"`, backtick, `$(...)`) run
130+
# as code. Routing through env + a heredoc write avoids both. (W-23178876)
131+
env:
132+
IN_CLIENT_ID: ${{ inputs.client-id }}
133+
IN_CLIENT_SECRET: ${{ inputs.client-secret }}
134+
IN_SERVER: ${{ inputs.server }}
135+
IN_CODE_VERSION: ${{ inputs.code-version }}
136+
IN_USERNAME: ${{ inputs.username }}
137+
IN_PASSWORD: ${{ inputs.password }}
138+
IN_SHORT_CODE: ${{ inputs.short-code }}
139+
IN_TENANT_ID: ${{ inputs.tenant-id }}
140+
IN_MRT_API_KEY: ${{ inputs.mrt-api-key }}
141+
IN_MRT_PROJECT: ${{ inputs.mrt-project }}
142+
IN_MRT_ENVIRONMENT: ${{ inputs.mrt-environment }}
143+
IN_ACCOUNT_MANAGER_HOST: ${{ inputs.account-manager-host }}
144+
IN_WEBDAV_SERVER: ${{ inputs.webdav-server }}
145+
IN_CERTIFICATE: ${{ inputs.certificate }}
146+
IN_CERTIFICATE_PASSPHRASE: ${{ inputs.certificate-passphrase }}
147+
IN_SELFSIGNED: ${{ inputs.selfsigned }}
148+
IN_LOG_LEVEL: ${{ inputs.log-level }}
121149
run: |
122150
# CI defaults
123151
echo "NO_COLOR=1" >> "$GITHUB_ENV"
124152
153+
# A random delimiter for the heredoc form below. Generated per-run so a
154+
# credential value can never accidentally contain (and thus prematurely
155+
# close) the delimiter line.
156+
ENV_DELIMITER="__B2C_ENV_$(openssl rand -hex 16 2>/dev/null || date +%s%N)__"
157+
158+
# Append a value to $GITHUB_ENV, but only when it is non-empty. Uses
159+
# GitHub's documented multiline heredoc syntax (NAME<<DELIM ... DELIM) —
160+
# the same mechanism @actions/core uses. The value is emitted via printf
161+
# as an ARGUMENT (never spliced into the format string or script text), so
162+
# it is written byte-for-byte: `$`, `%`, quotes, backticks AND embedded
163+
# newlines or `=` are all preserved literally, with no shell, printf, or
164+
# $GITHUB_ENV-format reinterpretation.
165+
set_env() {
166+
# $1 = target env var name, $2 = value
167+
if [ -n "$2" ]; then
168+
{
169+
printf '%s<<%s\n' "$1" "$ENV_DELIMITER"
170+
printf '%s\n' "$2"
171+
printf '%s\n' "$ENV_DELIMITER"
172+
} >> "$GITHUB_ENV"
173+
fi
174+
}
175+
125176
# Auth and config — only set if provided
126-
if [ -n "${{ inputs.client-id }}" ]; then
127-
echo "SFCC_CLIENT_ID=${{ inputs.client-id }}" >> "$GITHUB_ENV"
128-
fi
129-
if [ -n "${{ inputs.client-secret }}" ]; then
130-
echo "SFCC_CLIENT_SECRET=${{ inputs.client-secret }}" >> "$GITHUB_ENV"
131-
fi
132-
if [ -n "${{ inputs.server }}" ]; then
133-
echo "SFCC_SERVER=${{ inputs.server }}" >> "$GITHUB_ENV"
134-
fi
135-
if [ -n "${{ inputs.code-version }}" ]; then
136-
echo "SFCC_CODE_VERSION=${{ inputs.code-version }}" >> "$GITHUB_ENV"
137-
fi
138-
if [ -n "${{ inputs.username }}" ]; then
139-
echo "SFCC_USERNAME=${{ inputs.username }}" >> "$GITHUB_ENV"
140-
fi
141-
if [ -n "${{ inputs.password }}" ]; then
142-
echo "SFCC_PASSWORD=${{ inputs.password }}" >> "$GITHUB_ENV"
143-
fi
144-
if [ -n "${{ inputs.short-code }}" ]; then
145-
echo "SFCC_SHORTCODE=${{ inputs.short-code }}" >> "$GITHUB_ENV"
146-
fi
147-
if [ -n "${{ inputs.tenant-id }}" ]; then
148-
echo "SFCC_TENANT_ID=${{ inputs.tenant-id }}" >> "$GITHUB_ENV"
149-
fi
150-
if [ -n "${{ inputs.mrt-api-key }}" ]; then
151-
echo "MRT_API_KEY=${{ inputs.mrt-api-key }}" >> "$GITHUB_ENV"
152-
fi
153-
if [ -n "${{ inputs.mrt-project }}" ]; then
154-
echo "MRT_PROJECT=${{ inputs.mrt-project }}" >> "$GITHUB_ENV"
155-
fi
156-
if [ -n "${{ inputs.mrt-environment }}" ]; then
157-
echo "MRT_ENVIRONMENT=${{ inputs.mrt-environment }}" >> "$GITHUB_ENV"
158-
fi
159-
if [ -n "${{ inputs.account-manager-host }}" ]; then
160-
echo "SFCC_ACCOUNT_MANAGER_HOST=${{ inputs.account-manager-host }}" >> "$GITHUB_ENV"
161-
fi
162-
if [ -n "${{ inputs.webdav-server }}" ]; then
163-
echo "SFCC_WEBDAV_SERVER=${{ inputs.webdav-server }}" >> "$GITHUB_ENV"
164-
fi
165-
if [ -n "${{ inputs.certificate }}" ]; then
166-
echo "SFCC_CERTIFICATE=${{ inputs.certificate }}" >> "$GITHUB_ENV"
167-
fi
168-
if [ -n "${{ inputs.certificate-passphrase }}" ]; then
169-
echo "SFCC_CERTIFICATE_PASSPHRASE=${{ inputs.certificate-passphrase }}" >> "$GITHUB_ENV"
170-
fi
177+
set_env SFCC_CLIENT_ID "$IN_CLIENT_ID"
178+
set_env SFCC_CLIENT_SECRET "$IN_CLIENT_SECRET"
179+
set_env SFCC_SERVER "$IN_SERVER"
180+
set_env SFCC_CODE_VERSION "$IN_CODE_VERSION"
181+
set_env SFCC_USERNAME "$IN_USERNAME"
182+
set_env SFCC_PASSWORD "$IN_PASSWORD"
183+
set_env SFCC_SHORTCODE "$IN_SHORT_CODE"
184+
set_env SFCC_TENANT_ID "$IN_TENANT_ID"
185+
set_env MRT_API_KEY "$IN_MRT_API_KEY"
186+
set_env MRT_PROJECT "$IN_MRT_PROJECT"
187+
set_env MRT_ENVIRONMENT "$IN_MRT_ENVIRONMENT"
188+
set_env SFCC_ACCOUNT_MANAGER_HOST "$IN_ACCOUNT_MANAGER_HOST"
189+
set_env SFCC_WEBDAV_SERVER "$IN_WEBDAV_SERVER"
190+
set_env SFCC_CERTIFICATE "$IN_CERTIFICATE"
191+
set_env SFCC_CERTIFICATE_PASSPHRASE "$IN_CERTIFICATE_PASSPHRASE"
192+
set_env SFCC_LOG_LEVEL "$IN_LOG_LEVEL"
193+
171194
# Only export SFCC_SELFSIGNED when explicitly truthy. The SDK's env-var
172195
# source treats only "true"/"1" as enabling self-signed mode, but oclif
173196
# boolean flags map any non-empty env value to true. Skipping export for
174197
# other values avoids that asymmetry — leaving the flag at its default.
175-
if [ "${{ inputs.selfsigned }}" = "true" ] || [ "${{ inputs.selfsigned }}" = "1" ]; then
198+
if [ "$IN_SELFSIGNED" = "true" ] || [ "$IN_SELFSIGNED" = "1" ]; then
176199
echo "SFCC_SELFSIGNED=true" >> "$GITHUB_ENV"
177200
fi
178-
if [ -n "${{ inputs.log-level }}" ]; then
179-
echo "SFCC_LOG_LEVEL=${{ inputs.log-level }}" >> "$GITHUB_ENV"
180-
fi
181201
182202
- name: Install plugins
183203
if: inputs.plugins != ''

0 commit comments

Comments
 (0)