Skip to content

Pass inputs through env, so an input value cannot become shell - #9

Merged
pavel-te merged 3 commits into
mainfrom
harden-inputs
Aug 4, 2026
Merged

Pass inputs through env, so an input value cannot become shell#9
pavel-te merged 3 commits into
mainfrom
harden-inputs

Conversation

@pavel-te

@pavel-te pavel-te commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Every input except api-token was interpolated into the body of run: with ${{ }}. That substitution happens before bash parses the script, so an input carrying shell syntax is executed rather than read.

Verified locally under act with patterns: 'x"; id > /tmp/PWNED; echo "' — the injected command ran as root inside the job.

Affected

config-file, source-locale, patterns, api-url, file-tag-name, monitor-interval, monitor-max-attempts. Each now travels as an env var and is dereferenced as "$INPUT_*" — the treatment api-token already had. github.action_path moves along with them.

Reachability

The calling workflow controls these values, so this is not exploitable from outside a repository as written. It becomes exploitable as soon as a caller wires something untrusted into one of them:

- uses: OnTheGoSystems/ptc-action@v1
  with:
    patterns: ${{ github.event.pull_request.title }}   # attacker-controlled

That is a normal-looking workflow, and it would hand arbitrary shell to anyone who can open a pull request.

Regression guard

The self-test gains a check that fails if any expression is interpolated into a run: body again. Against the previous revision it reports all 12 occurrences.

Verification

Local act suite against this branch — two happy paths (committed config, auto-detect), four must-fail paths (bad token, terminal failure, soft-fail upload, undetectable layout), and an injection probe that asserts the payload did not run: all green. The same probe fails on main, which is what makes it a gate rather than a comment.

Not in this PR: add-paths: '.' still sweeps the whole working directory into the translation PR. Real, but functional rather than security, and it deserves its own change so this one stays reviewable.

`${{ inputs.* }}` is substituted into the body of `run:` before bash
parses it, so an input carrying shell syntax is executed rather than
read. Verified locally: `patterns: 'x"; id > /tmp/PWNED; echo "'` ran as
root inside the job.

Seven inputs were affected — config-file, source-locale, patterns,
api-url, file-tag-name, monitor-interval, monitor-max-attempts. Each now
travels as an env var and is dereferenced as "$INPUT_*", which is the
treatment api-token already had. `github.action_path` moves with them.

The caller controls these values today, so this is not exploitable from
outside a repository. It becomes exploitable the moment a workflow wires
something untrusted into one of them, e.g.
`patterns: ${{ github.event.pull_request.title }}`.

The self-test gains a check that fails if any expression is interpolated
into a run: body again — it flags all 12 occurrences on the previous
revision.

No behaviour change: the local act suite (two happy paths, four
must-fail paths, one injection probe) is green.
An unquoted `run:` inside a step name is a mapping value to the YAML
parser, so the whole workflow failed to load.
A run body is interpolated too, so spelling the delimiters literally
inside it made the workflow fail to load — the check meant to ban
interpolation could not itself survive being written down. The pattern is
now assembled at runtime, and the explanatory comment names the
delimiters instead of quoting them.
@pavel-te
pavel-te merged commit b9ee900 into main Aug 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant