Skip to content

enhancement(release/github): env-scope caller inputs in run: blocks to remove script-injection surface #29

Description

@MAfarrag

Context

Follow-up hardening item (internal tracking id: SEC-8) surfaced by the security review that produced PR #28.
That PR moved caller-controlled inputs out of run: script bodies and into env: blocks for the
python-setup/pip, python-setup/uv, python-setup/pixi, and release/pypi actions, and removed eval.
actions/release/github/action.yml was intentionally left out of that PR's scope, so it still interpolates
inputs.* directly into shell script text — the same anti-pattern PR #28 fixed everywhere else.

Problem / Current Behaviour

GitHub Actions substitutes ${{ ... }} expressions into the run: script text before bash executes.
A value containing shell metacharacters ($( ), backticks, ;, |) therefore becomes part of the script
and runs as code. release/github interpolates several caller-controlled inputs directly into run: bodies,
including into executed command strings and a command substitution.

These inputs come from the calling workflow (normally a trusted maintainer), so this is not directly
attacker-controlled today — severity is defense-in-depth. It becomes exploitable if a consumer wires one of
these inputs from untrusted data (e.g. config-file: ${{ github.event.client_payload.path }}). It is flagged
for consistency: the same anti-pattern was already removed from the sibling actions.

Affected locations

All in actions/release/github/action.yml (line numbers as of the security-hardening branch / post-PR #28):

Step Lines Sink Input(s)
Generate changelog and bump version 340, 344, 368, 371, 400, 402 $CZ_CMD bump ... --increment X --prerelease Y (executed, unquoted) increment, prerelease-type
Validate changelog / Generate changelog 226, 286, 288 CZ_CMD="pixi run -e <install-groups> cz" (later executed) install-groups
Validate changelog / Generate changelog 228, 290 CZ_CMD="<package-manager> run cz" (later executed) package-manager
Validate changelog / Generate changelog 233, 298 WORK_DIR=$(dirname "<config-file>") then cd "$WORK_DIR" config-file
Display release configuration 157–174 echo of inputs (command-substitution-in-echo) multiple
Fold refreshed lockfile 476 case "<package-manager>" in package-manager
Create GitHub Release / Release summary 550, 623–627 echo of inputs draft, increment, prerelease-type

Safe and out of scope: if: conditions (e.g. lines 183, 190, 198, 446, 603) and with: inputs
(186–203, 606–610) are GitHub-expression contexts, not shell — they need no change.

Steps to Reproduce / Motivation Example

# A consumer workflow that pipes untrusted data into an input reaches a shell context:
- uses: serapeum-org/github-actions/actions/release/github@release/github/v1
  with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    config-file: ${{ github.event.client_payload.path }}   # attacker-influenced -> RCE on the runner

With config-file set to $(malicious), the WORK_DIR=$(dirname "$(malicious)") line executes it.

Proposed Solution

Same mechanical treatment applied in PR #28: for every step that references ${{ inputs.* }} inside its
run: body, add the value to that step's env: block and reference the quoted shell variable instead.

# Before
- name: Generate changelog and bump version with commitizen
  shell: bash
  run: |
    NEXT_VERSION=$($CZ_CMD bump --yes --increment ${{ inputs.increment }} \
                            --prerelease ${{ inputs.prerelease-type }} ...)

# After
- name: Generate changelog and bump version with commitizen
  shell: bash
  env:
    INCREMENT: ${{ inputs.increment }}
    PRERELEASE_TYPE: ${{ inputs.prerelease-type }}
    PACKAGE_MANAGER: ${{ inputs.package-manager }}
    INSTALL_GROUPS: ${{ inputs.install-groups }}
    CONFIG_FILE: ${{ inputs.config-file }}
  run: |
    NEXT_VERSION=$($CZ_CMD bump --yes --increment "$INCREMENT" \
                            --prerelease "$PRERELEASE_TYPE" ...)

Behavior is unchanged; only the substitution mechanism changes.

Out of Scope

Effort Estimate

Size: S
Rationale: single file; ~6 run: steps get an env: block and quoted-variable substitutions, plus a
CI re-run — no logic changes.

Definition of Done

  • No ${{ inputs.* }} expression remains inside any run: body in actions/release/github/action.yml
    (grep-verifiable; if:/with: occurrences excluded).
  • Each moved input is referenced as a quoted shell variable.
  • actions/release/github/action.yml parses as valid YAML and all rewritten run: blocks pass bash -n.
  • The test-release-github.yml suite passes unchanged (behavior preserved).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions