Coordinated Disclosure Timeline
- 2023-12-18: Opened GHSA-ghm2-rq8q-wrhc through Private Vulnerability Reporting.
- 2023-12-28: Fixed in 498d3f3
Summary
The tj-actions/verify-changed-files
workflow allows for command injection in changed filenames, potentially allowing an attacker to leak secrets.
Project
verify-changed-files
Tested Version
Details
Potential Actions command injection in output filenames (GHSL-2023-275
)
The verify-changed-files
workflow returns the list of files changed in a commit or pull request.
if [[ -n "$INPUT_FILES_PATTERN_FILE" ]]; then
TRACKED_FILES=$(git diff --diff-filter=ACMUXTR --name-only | { grep -x -E -f "$INPUT_FILES_PATTERN_FILE" || true; } | awk -v d="|" '{s=(NR==1?s:s d)$0}END{print s}')
# Find untracked changes
# shellcheck disable=SC2086
UNTRACKED_OR_IGNORED_FILES=$(git status $GIT_STATUS_EXTRA_ARGS | awk '{print $NF}' | { grep -x -E -f "$INPUT_FILES_PATTERN_FILE" || true; } | awk -v d="|" '{s=(NR==1?s:s d)$0}END{print s}')
# Find unstaged deleted files
UNSTAGED_DELETED_FILES=$(git ls-files --deleted | { grep -x -E -f "$INPUT_FILES_PATTERN_FILE" || true; } | awk -v d="|" '{s=(NR==1?s:s d)$0}END{print s}')
else
TRACKED_FILES=$(git diff --diff-filter=ACMUXTR --name-only | awk -v d="|" '{s=(NR==1?s:s d)$0}END{print s}')
# Find untracked changes
# shellcheck disable=SC2086
UNTRACKED_OR_IGNORED_FILES=$(git status $GIT_STATUS_EXTRA_ARGS | awk '{print $NF}' | awk -v d="|" '{s=(NR==1?s:s d)$0}END{print s}')
# Find unstaged deleted files
UNSTAGED_DELETED_FILES=$(git ls-files --deleted | awk -v d="|" '{s=(NR==1?s:s d)$0}END{print s}')
fi
Given that there is no sanitization being applied before appending to $GITHUB_OUTPUT
, it allows for filenames to contain special characters such as ;
and ` (backtick) which can be used by an attacker to take over the GitHub Runner if the output value is used in a raw fashion (thus being directly replaced before execution) inside a run
block. By running custom commands an attacker may be able to steal secrets such as GITHUB_TOKEN
if triggered on other events than pull_request
. For example on push
.
if [[ -n "$CHANGED_FILES" ]]; then
echo "Found uncommited changes"
CHANGED_FILES=$(echo "$CHANGED_FILES" | awk '{gsub(/\|/,"\n"); print $0;}' | awk -v d="$INPUT_SEPARATOR" '{s=(NR==1?s:s d)$0}END{print s}')
echo "files_changed=true" >> "$GITHUB_OUTPUT"
echo "changed_files=$CHANGED_FILES" >> "$GITHUB_OUTPUT"
...
Proof of Concept
In the case of a repository containing the following steps, as detailed in verify-changed-files
README:
- name: Verify Changed files
uses: tj-actions/verify-changed-files@v16
id: verify-changed-files
- name: List all changed files tracked and untracked files
run: |
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
- Submit a pull request to the repository with a new file injecting a command. For example
$(whoami).txt
would be a valid filename. - Upon approval of the workflow (triggered by the pull request), the action will get executed and the malicious pull request filename will flow into the
List all changed files tracked and untracked files
step.
##[group]Run echo "Changed files: $(whoami).txt"
echo "Changed files: $(whoami).txt"
shell: /usr/bin/bash -e {0}
##[endgroup]
Changed files: runner.txt
Impact
This issue may lead to arbitrary command execution in the GitHub Runner.
Credit
This issue was discovered and reported by GHSL team member @jorgectf (Jorge Rosillo).
Contact
You can contact the GHSL team at [email protected]
, please include a reference to GHSL-2023-275
in any communication regarding this issue.