fix: harden GitHub Actions shell interpolation

This commit is contained in:
Michael Bolin
2026-03-16 16:08:46 -07:00
parent dbe3d52c04
commit bd50fa400f
4 changed files with 87 additions and 36 deletions
+6 -2
View File
@@ -37,10 +37,14 @@ jobs:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Pre-fetch base and head refs for the PR
env:
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Pass GitHub expressions through env and quote shell expansions.
git fetch --no-tags origin \
${{ github.event.pull_request.base.ref }} \
+refs/pull/${{ github.event.pull_request.number }}/head
"$PR_BASE_REF" \
"+refs/pull/$PR_NUMBER/head"
# If you want Codex to build and run code, install any dependencies that
# need to be downloaded before the "Run Codex" step because Codex's
+64 -33
View File
@@ -107,8 +107,10 @@ runs:
- name: Validate Windows safety strategy
if: ${{ runner.os == 'Windows' }}
shell: bash
env:
SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
run: |
if [ "${{ inputs['safety-strategy'] }}" != "unsafe" ]; then
if [ "$SAFETY_STRATEGY" != "unsafe" ]; then
echo "On Windows, inputs['safety-strategy'] must be 'unsafe'" >&2
echo "because no viable sandboxing options are available at this time." >&2
exit 1
@@ -123,46 +125,63 @@ runs:
- name: Check repository write access
env:
ACTION_PATH: ${{ github.action_path }}
GITHUB_TOKEN: ${{ github.token }}
ALLOW_BOTS: ${{ inputs['allow-bots'] }}
ALLOW_USERS: ${{ inputs['allow-users'] }}
shell: bash
run: |
node "${{ github.action_path }}/dist/main.js" check-write-access \
--allow-bots "${{ inputs['allow-bots'] }}" \
--allow-users "${{ inputs['allow-users'] }}"
node "$ACTION_PATH/dist/main.js" check-write-access \
--allow-bots "$ALLOW_BOTS" \
--allow-users "$ALLOW_USERS"
- name: Install Codex CLI
shell: bash
run: npm install -g "@openai/codex@${{ inputs['codex-version'] }}"
env:
CODEX_VERSION: ${{ inputs['codex-version'] }}
run: npm install -g "@openai/codex@${CODEX_VERSION}"
- name: Install Codex Responses API proxy
shell: bash
run: npm install -g "@openai/codex-responses-api-proxy@${{ inputs['codex-version'] }}"
env:
CODEX_VERSION: ${{ inputs['codex-version'] }}
run: npm install -g "@openai/codex-responses-api-proxy@${CODEX_VERSION}"
- name: Resolve Codex home
id: resolve_home
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
CODEX_HOME_OVERRIDE: ${{ inputs['codex-home'] }}
SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
CODEX_USER: ${{ inputs['codex-user'] }}
CODEX_RUN_ID: ${{ github.run_id }}
run: |
node "${{ github.action_path }}/dist/main.js" resolve-codex-home \
--codex-home-override "${{ inputs['codex-home'] }}" \
--safety-strategy "${{ inputs['safety-strategy'] }}" \
--codex-user "${{ inputs['codex-user'] }}" \
--github-run-id "${{ github.run_id }}"
node "$ACTION_PATH/dist/main.js" resolve-codex-home \
--codex-home-override "$CODEX_HOME_OVERRIDE" \
--safety-strategy "$SAFETY_STRATEGY" \
--codex-user "$CODEX_USER" \
--github-run-id "$CODEX_RUN_ID"
- name: Determine server info path
id: derive_server_info
shell: bash
env:
CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
CODEX_RUN_ID: ${{ github.run_id }}
run: |
server_info_file="${{ steps.resolve_home.outputs.codex-home }}/${{ github.run_id }}.json"
server_info_file="$CODEX_HOME/$CODEX_RUN_ID.json"
echo "server_info_file=$server_info_file" >> "$GITHUB_OUTPUT"
- name: Check Responses API proxy status
id: start_proxy
if: ${{ inputs['openai-api-key'] != '' }}
shell: bash
env:
SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
run: |
server_info_file="${{ steps.derive_server_info.outputs.server_info_file }}"
if [ -s "$server_info_file" ]; then
echo "Responses API proxy already appears to be running (found $server_info_file)."
if [ -s "$SERVER_INFO_FILE" ]; then
echo "Responses API proxy already appears to be running (found $SERVER_INFO_FILE)."
echo "server_info_file_exists=true" >> "$GITHUB_OUTPUT"
else
echo "server_info_file_exists=false" >> "$GITHUB_OUTPUT"
@@ -175,19 +194,19 @@ runs:
- name: Start Responses API proxy
if: ${{ inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
env:
SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
PROXY_API_KEY: ${{ inputs['openai-api-key'] }}
UPSTREAM_URL: ${{ inputs['responses-api-endpoint'] }}
shell: bash
run: |
upstream_url="${{ inputs['responses-api-endpoint'] }}"
args=(
codex-responses-api-proxy
--http-shutdown
--server-info "${{ steps.derive_server_info.outputs.server_info_file }}"
--server-info "$SERVER_INFO_FILE"
)
if [ -n "$upstream_url" ]; then
args+=(--upstream-url "$upstream_url")
if [ -n "$UPSTREAM_URL" ]; then
args+=(--upstream-url "$UPSTREAM_URL")
fi
(
@@ -197,22 +216,23 @@ runs:
- name: Wait for Responses API proxy
if: ${{ inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
shell: bash
env:
SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
run: |
server_info_file="${{ steps.derive_server_info.outputs.server_info_file }}"
for _ in {1..10}; do
if [ -s "$server_info_file" ]; then
if [ -s "$SERVER_INFO_FILE" ]; then
break
fi
sleep 1
done
if [ ! -s "$server_info_file" ]; then
if [ ! -s "$SERVER_INFO_FILE" ]; then
echo "responses-api-proxy did not write server info" >&2
exit 1
fi
if [ "${RUNNER_OS}" != "Windows" ]; then
sudo chmod 444 "$server_info_file"
sudo chown root "$server_info_file"
sudo chmod 444 "$SERVER_INFO_FILE"
sudo chown root "$SERVER_INFO_FILE"
fi
# This step has an output named `port`.
@@ -220,27 +240,37 @@ runs:
id: read_server_info
if: ${{ inputs['openai-api-key'] != '' || inputs.prompt != '' || inputs['prompt-file'] != '' }}
shell: bash
run: node "${{ github.action_path }}/dist/main.js" read-server-info "${{ steps.derive_server_info.outputs.server_info_file }}"
env:
ACTION_PATH: ${{ github.action_path }}
SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
run: node "$ACTION_PATH/dist/main.js" read-server-info "$SERVER_INFO_FILE"
- name: Write Codex proxy config
if: ${{ inputs['openai-api-key'] != '' }}
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
PROXY_PORT: ${{ steps.read_server_info.outputs.port }}
SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
run: |
node "${{ github.action_path }}/dist/main.js" write-proxy-config \
--codex-home "${{ steps.resolve_home.outputs.codex-home }}" \
--port "${{ steps.read_server_info.outputs.port }}" \
--safety-strategy "${{ inputs['safety-strategy'] }}"
node "$ACTION_PATH/dist/main.js" write-proxy-config \
--codex-home "$CODEX_HOME" \
--port "$PROXY_PORT" \
--safety-strategy "$SAFETY_STRATEGY"
- name: Drop sudo privilege, if appropriate
if: ${{ inputs['safety-strategy'] == 'drop-sudo' && inputs['openai-api-key'] != '' }}
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
run: |
case "${RUNNER_OS}" in
Linux)
node "${{ github.action_path }}/dist/main.js" drop-sudo --user runner --group sudo
node "$ACTION_PATH/dist/main.js" drop-sudo --user runner --group sudo
;;
macOS)
node "${{ github.action_path }}/dist/main.js" drop-sudo --user runner --group admin
node "$ACTION_PATH/dist/main.js" drop-sudo --user runner --group admin
;;
*)
echo "Unsupported OS for drop-sudo: ${RUNNER_OS}" >&2
@@ -275,10 +305,11 @@ runs:
CODEX_EFFORT: ${{ inputs.effort }}
CODEX_SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
CODEX_USER: ${{ inputs['codex-user'] }}
ACTION_PATH: ${{ github.action_path }}
FORCE_COLOR: 1
shell: bash
run: |
node "${{ github.action_path }}/dist/main.js" run-codex-exec \
node "$ACTION_PATH/dist/main.js" run-codex-exec \
--prompt "${CODEX_PROMPT}" \
--prompt-file "${CODEX_PROMPT_FILE}" \
--output-file "$CODEX_OUTPUT_FILE" \
+14
View File
@@ -16,6 +16,20 @@ There is a lot of valuable context that can be used to fuel your invocation of C
- **Commit messages**: a pull request can be composed of many commits. The messages for individual commits often go unnoticed, but could read by Codex.
- **Screenshots** screenshots and other media have been known to be used as vehicles for prompt injection.
## Avoid shell injection in workflow steps
GitHub Actions expands `${{ ... }}` expressions before the shell runs your `run:` script. If you splice untrusted values such as branch names, issue titles, comment bodies, or action inputs directly into the script, those values can break shell quoting and execute arbitrary commands.
Instead, pass those values through `env:` and quote the shell variables that consume them:
```yaml
- name: Safe shell usage
env:
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
git fetch origin "$PR_BASE_REF"
```
<!-- TODO ## Protecting secrets -->
## Look out for API key abuse
+3 -1
View File
@@ -29,9 +29,11 @@ jobs:
safety-strategy: ${{ matrix.safety-strategy }}
- name: Try to dump the key from the codex-responses-api-proxy process
env:
CODEX_RUN_ID: ${{ github.run_id }}
run: |
# Find the PID for the codex-responses-api-proxy process.
SERVER_INFO_FILE="$HOME/.codex/${{ github.run_id }}.json"
SERVER_INFO_FILE="$HOME/.codex/$CODEX_RUN_ID.json"
PID=$(jq .pid < "$SERVER_INFO_FILE")
# Using standard filesystem read operations (albeit privileged ones),