Skip to content

ci: pass PR title and changed files through env in scoring workflow - #2481

Open
arpitjain099 wants to merge 1 commit into
brain-score:masterfrom
arpitjain099:fix/workflow-command-injection
Open

arpitjain099 wants to merge 1 commit into
brain-score:masterfrom
arpitjain099:fix/workflow-command-injection

Conversation

@arpitjain099

Copy link
Copy Markdown

Following up on the report I sent Kartik privately, opening it here as asked.

What

score_new_plugins.yml interpolates ${{ github.event.pull_request.title }} and ${{ env.CHANGED_FILES }} directly into run: scripts. Both are attacker controlled. The title is free text; CHANGED_FILES is built from git diff --name-only, so it carries filenames taken from the pull request.

GitHub substitutes those expressions as literal text into the script before bash runs, so a value that closes the surrounding quotes is executed rather than read.

Verified rather than assumed

Both tested locally on reconstructions of these exact shell lines. Nothing was run against Brain-Score infrastructure.

PR title. In the Update PLUGIN_INFO based on label step:

BS_UID="$(echo '${{ github.event.pull_request.title }}' | sed ...)"

A title of x'; touch /tmp/title_pwn; echo 'y (user:bob) (public:True) closed the quote and the touch ran, while the sed still produced a plausible BS_UID, so the log looks normal afterwards. Titles have no character restrictions.

Filename. In Get plugin info. Worth noting git's quoting is less protective than it looks: it quotes and escapes a path containing a double quote, so that payload fails, but it leaves single quotes, backticks and $( ) untouched. A file named a'; touch bsx_marker; echo 'b.py closed the python -c string and ran. Python then raises a SyntaxError, but only after the injected command has executed, so the failure reads as a malformed filename. This one is bounded, since filenames cannot contain /.

Why it is worth fixing

Configure AWS Credentials runs earlier in the same job, so anything executing in these steps does so with those credentials already in the environment.

The workflow is pull_request_target on closed with if: merged == true, so a PR has to be merged first and an outside contributor cannot self-label. The route that remains is a brain-score.org submission, which arrives already carrying automerge-web.

The change

Nine interpolation sites moved into env: blocks and referenced as shell variables, which GitHub does not evaluate as script. PLUGIN_INFO is a step output rather than an environment variable, so the steps consuming it get explicit env: entries. CHANGED_FILES is already in $GITHUB_ENV. No behaviour change otherwise, and the YAML validates.

Also worth a look

brain-score/language has the same pattern in plugin_submission_orchestrator.yml: PR_TITLE="${{ github.event.pull_request.title }}" at lines 251, 823, 868, 1044 and 1080, and CHANGED_FILES at line 106. The double-quoted form is injectable the same way, which I also confirmed locally. Happy to send a matching PR there if useful.

Separately and unrelated to injection: hmarr/auto-approve-action@v3.1.0 and plm9606/automerge_actions@1.2.2 in automerge_plugin-only_prs.yml are mutable tags rather than commit SHAs, and the second is handed WORKFLOW_TOKEN. Left out of this PR to keep it focused, but happy to pin them in a follow-up.

I used AI tooling while investigating and drafting this; the findings were reproduced and verified by hand before opening.

The scoring workflow interpolates ${{ github.event.pull_request.title }} and
${{ env.CHANGED_FILES }} directly into run: scripts. Both are attacker
controlled: the title is free text, and CHANGED_FILES is built from
git diff --name-only, so it carries filenames taken from the pull request.

A title containing a single quote closes the surrounding quotes in the
BS_UID and BS_PUBLIC steps and the remainder is executed by the shell.
Filenames behave the same way in the Get plugin info step: git quotes a path
containing a double quote and escapes it, but leaves single quotes, backticks
and $( ) untouched, so a single quote closes the python -c string. Python
then raises a SyntaxError, but only after the injected command has run, so the
failure looks like a malformed filename in the log.

This matters because Configure AWS Credentials runs earlier in the same job,
so anything executing here does so with those credentials in the environment.

Pass both values through env: blocks and reference them as shell variables,
which GitHub does not evaluate as script. PLUGIN_INFO is a step output rather
than an environment variable, so the steps that consume it get explicit env
entries. No behaviour change otherwise.

Signed-off-by: Arpit Jain <arpitjain099@gmail.com>

This branch has not been deployed

No deployments
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