VYPR
High severityNVD Advisory· Published Dec 29, 2023· Updated Aug 2, 2024

GitHub Action tj-actions/verify-changed-files is vulnerable to command injection in output filenames

CVE-2023-52137

Description

Command injection in tj-actions/verify-changed-files due to unsanitized filenames can allow arbitrary code execution on GitHub runners.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Command injection in tj-actions/verify-changed-files due to unsanitized filenames can allow arbitrary code execution on GitHub runners.

The tj-actions/verify-changed-files GitHub Action lists changed files during a workflow execution. The vulnerability arises because filenames containing special characters (e.g., ;) are returned as-is in the changed_files output. If a workflow uses this output directly in a run block without proper escaping, an attacker can inject arbitrary commands [1].

An attacker can create a pull request with a file that has a malicious name, such as ; echo "compromised"; .txt. When the action runs, the output includes this filename. If the workflow then executes echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}" without using an environment variable, the injected command will execute on the GitHub runner [2]. The attack requires that the workflow triggers on events like pull_request and that the output is used unsafely.

Successful exploitation allows an attacker to run arbitrary commands with the permissions of the runner, potentially stealing secrets such as GITHUB_TOKEN or other environment variables. This could lead to compromise of the entire repository or other resources accessible to the token [1].

The vulnerability is patched in versions v17 and v17.0.0 by enabling safe_output by default, which escapes special characters for bash environments. The patch also adds escaping logic for characters like $, (, ), ` `, |, &, and ;` [3][4]. Users should update to the latest version and follow best practices by using environment variables to capture the output instead of direct substitution.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
tj-actions/verify-changed-filesGitHub Actions
< 1717

Affected products

2

Patches

2
592e305da041

chore: update entrypoint.sh (#357)

1 file changed · +15 17
  • entrypoint.sh+15 17 modified
    @@ -39,17 +39,26 @@ fi
     
     CHANGED_FILES=""
     
    -# Function to concatenate non-empty strings with a separator
    +# Function to concatenate non-empty file names with a separator
     concatenate() {
       local separator=$1
       shift
       local result=""
    -  for str in "$@"; do
    -    if [[ -n $str ]]; then
    +  for filename in "$@"; do
    +    if [[ "$INPUT_SAFE_OUTPUT" == "true" ]]; then
    +      filename=${filename//$/\\$} # Replace $ with \$
    +      filename=${filename//\(/\\\(} # Replace ( with \(
    +      filename=${filename//\)/\\\)} # Replace ) with \)
    +      filename=${filename//\`/\\\`} # Replace ` with \`
    +      filename=${filename//|/\\|} # Replace | with \|
    +      filename=${filename//&/\\&} # Replace & with \&
    +      filename=${filename//;/\\;} # Replace ; with \;
    +    fi
    +    if [[ -n $filename ]]; then
           if [[ -n $result ]]; then
    -        result+="$separator$str"
    +        result+="$separator$filename"
           else
    -        result="$str"
    +        result="$filename"
           fi
         fi
       done
    @@ -62,20 +71,10 @@ CHANGED_FILES=$(concatenate "|" "$TRACKED_FILES" "$UNTRACKED_OR_IGNORED_FILES" "
     CHANGED_FILES=$(echo "$CHANGED_FILES"  | awk '{gsub(/\|/,"\n"); print $0;}' | sort -u | awk -v d="|" '{s=(NR==1?s:s d)$0}END{print s}')
     
     if [[ -n "$CHANGED_FILES" ]]; then
    -  echo "Found uncommited changes"
    +  echo "Found uncommitted 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}')
     
    -  if [[ "$INPUT_SAFE_OUTPUT" == "true" ]]; then
    -    CHANGED_FILES=${CHANGED_FILES//$/\\$} # Replace $ with \$
    -    CHANGED_FILES=${CHANGED_FILES//\(/\\\(}} # Replace ( with \(
    -    CHANGED_FILES=${CHANGED_FILES//\)/\\\)}} # Replace ) with \)
    -    CHANGED_FILES=${CHANGED_FILES//\`/\\\`} # Replace ` with \`
    -    CHANGED_FILES=${CHANGED_FILES//|/\\|} # Replace | with \|
    -    CHANGED_FILES=${CHANGED_FILES//&/\\&} # Replace & with \&
    -    CHANGED_FILES=${CHANGED_FILES//;/\\;} # Replace ; with \;
    -  fi
    -
       echo "files_changed=true" >> "$GITHUB_OUTPUT"
       echo "changed_files=$CHANGED_FILES" >> "$GITHUB_OUTPUT"
     
    @@ -85,7 +84,6 @@ if [[ -n "$CHANGED_FILES" ]]; then
         fi
         exit 1
       fi
    -
     else
       echo "No changes found."
       echo "files_changed=false" >> "$GITHUB_OUTPUT"
    
498d3f316f50

Merge pull request from GHSA-ghm2-rq8q-wrhc

3 files changed · +27 2
  • action.yml+5 0 modified
    @@ -25,6 +25,10 @@ inputs:
         description: 'Message to display when files have changed and the `fail-if-changed` input is set to `true`.'
         default: "Files have changed."
         required: false
    +  safe_output:
    +    description: "Apply sanitization to output filenames before being set as output."
    +    required: false
    +    default: "true"
     
     outputs:
       files_changed:
    @@ -61,6 +65,7 @@ runs:
             INPUT_MATCH_GITIGNORE_FILES: ${{ inputs.match-gitignore-files }}
             INPUT_FAIL_IF_CHANGED: ${{ inputs.fail-if-changed }}
             INPUT_FAIL_MSG: ${{ inputs.fail-message }}
    +        INPUT_SAFE_OUTPUT: ${{ inputs.safe_output }}
     
     branding:
       icon: file-text
    
  • entrypoint.sh+10 0 modified
    @@ -66,6 +66,16 @@ if [[ -n "$CHANGED_FILES" ]]; then
     
       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}')
     
    +  if [[ "$INPUT_SAFE_OUTPUT" == "true" ]]; then
    +    CHANGED_FILES=${CHANGED_FILES//$/\\$} # Replace $ with \$
    +    CHANGED_FILES=${CHANGED_FILES//\(/\\\(}} # Replace ( with \(
    +    CHANGED_FILES=${CHANGED_FILES//\)/\\\)}} # Replace ) with \)
    +    CHANGED_FILES=${CHANGED_FILES//\`/\\\`} # Replace ` with \`
    +    CHANGED_FILES=${CHANGED_FILES//|/\\|} # Replace | with \|
    +    CHANGED_FILES=${CHANGED_FILES//&/\\&} # Replace & with \&
    +    CHANGED_FILES=${CHANGED_FILES//;/\\;} # Replace ; with \;
    +  fi
    +
       echo "files_changed=true" >> "$GITHUB_OUTPUT"
       echo "changed_files=$CHANGED_FILES" >> "$GITHUB_OUTPUT"
     
    
  • README.md+12 2 modified
    @@ -60,6 +60,7 @@ Verify that certain files or directories did or did not change during the workfl
             uses: tj-actions/verify-changed-files@v16
             id: verify-changed-files
             with:
    +          safe_output: false # true by default, set to false because we are using an environment variable to store the output and avoid command injection.
               files: |
                  *.txt
                  test_directory
    @@ -69,8 +70,11 @@ Verify that certain files or directories did or did not change during the workfl
     
           - name: Run step only when any of the above files change.
             if: steps.verify-changed-files.outputs.files_changed == 'true'
    +        env:
    +          FILES_CHANGED: |-
    +            ${{ steps.verify-changed-files.outputs.changed_files }}
             run: |
    -          echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
    +          echo "Changed files: $FILES_CHANGED"
             # Outputs: "Changed files: new.txt test_directory/new.txt"
     ```
     
    @@ -82,6 +86,7 @@ Verify that certain files or directories did or did not change during the workfl
             uses: tj-actions/verify-changed-files@v16
             id: verify-changed-files
             with:
    +          safe_output: false
               files: |
                  new.txt
                  test_directory
    @@ -99,10 +104,15 @@ Verify that certain files or directories did or did not change during the workfl
           - name: Verify Changed files
             uses: tj-actions/verify-changed-files@v16
             id: verify-changed-files
    +        with:
    +          safe_output: false
           
           - name: List all changed files tracked and untracked files
    +        env:
    +          FILES_CHANGED: |-
    +            ${{ steps.verify-changed-files.outputs.changed_files }}
             run: |
    -          echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
    +          echo "Changed files: $FILES_CHANGED"
     ```
     
     If you feel generous and want to show some extra appreciation:
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.