VYPR
High severityNVD Advisory· Published Dec 27, 2023· Updated Sep 25, 2024

tj-actions/changed-files command injection in output filenames

CVE-2023-51664

Description

Command injection vulnerability in tj-actions/changed-files before v41.0.0 allows arbitrary code execution via malicious filenames in pull requests, potentially leaking secrets.

AI Insight

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

Command injection vulnerability in tj-actions/changed-files before v41.0.0 allows arbitrary code execution via malicious filenames in pull requests, potentially leaking secrets.

Overview

CVE-2023-51664 is a command injection vulnerability in the tj-actions/changed-files GitHub Action prior to version 41.0.0. The action retrieves a list of changed files from commits or pull requests, but it did not properly sanitize filenames that contain special shell characters such as ;, ` `, $, or (). When workflow steps used the raw output (e.g., ${{ steps.changed-files.outputs.all_changed_files }}) directly in a run` block without escaping, those filenames were interpolated before execution, allowing injected commands to run on the GitHub Runner.[1][4]

Exploitation

An attacker can trigger exploitation by submitting a pull request containing a file with a crafted name, such as $(whoami).txt. Once the workflow runs (on pull_request or other events), the action returns that filename, and if the workflow uses it unsafely in a shell command, the injected command executes. The attacker does not need any special privileges beyond being able to open a pull request. The vulnerability is especially impactful when triggered on events like push where secrets (e.g., GITHUB_TOKEN) may be accessible.[4]

Impact

Successful exploitation grants the attacker arbitrary command execution within the GitHub Runner environment. This can lead to compromise of CI/CD secrets, unauthorized access to repositories, and further lateral movement. The advisory notes that this could result in "arbitrary command execution in the GitHub Runner" and potential secret leakage.[2][3]

Mitigation

The issue is fixed in version 41.0.0. The fix introduces a safe_output input (enabled by default) that escapes dangerous characters, and the recommended usage pattern stores outputs in environment variables rather than substituting them directly into shell commands. Users are advised to upgrade to v41.0.0 or later, and to follow the secure usage examples provided in the advisory.[1][2][4]

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/changed-filesGitHub Actions
< 4141

Affected products

2

Patches

3
716b1e130428

fix: update characters escaped by safe output (#1815)

https://github.com/tj-actions/changed-filesTonye JackDec 24, 2023via ghsa
3 files changed · +3 6
  • dist/index.js+1 1 modified
    @@ -2829,7 +2829,7 @@ const setOutput = ({ key, value, writeOutputFiles, outputDir, json = false, shou
         }
         // if safeOutput is true, escape special characters for bash shell
         if (safeOutput) {
    -        cleanedValue = cleanedValue.replace(/[^\x20-\x7E]|[:*?"<>|;`$()&!]/g, '\\$&');
    +        cleanedValue = cleanedValue.replace(/[^\x20-\x7E]|[:*?<>|;`$()&!]/g, '\\$&');
         }
         core.setOutput(key, cleanedValue);
         if (writeOutputFiles) {
    
  • dist/index.js.map+1 1 modified
  • src/utils.ts+1 4 modified
    @@ -1355,10 +1355,7 @@ export const setOutput = async ({
     
       // if safeOutput is true, escape special characters for bash shell
       if (safeOutput) {
    -    cleanedValue = cleanedValue.replace(
    -      /[^\x20-\x7E]|[:*?"<>|;`$()&!]/g,
    -      '\\$&'
    -    )
    +    cleanedValue = cleanedValue.replace(/[^\x20-\x7E]|[:*?<>|;`$()&!]/g, '\\$&')
       }
     
       core.setOutput(key, cleanedValue)
    
ff2f6e6b9191

fix: update safe output regex and the docs (#1805)

https://github.com/tj-actions/changed-filestj-actions[bot]Dec 23, 2023via ghsa
4 files changed · +44 16
  • dist/index.js+33 14 modified
    @@ -454,7 +454,8 @@ const setOutputsAndGetModifiedAndChangedFilesStatus = ({ allDiffFiles, allFilter
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
         yield (0, utils_1.setOutput)({
             key: (0, utils_1.getOutputKey)('added_files_count', outputPrefix),
    @@ -474,7 +475,8 @@ const setOutputsAndGetModifiedAndChangedFilesStatus = ({ allDiffFiles, allFilter
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
         yield (0, utils_1.setOutput)({
             key: (0, utils_1.getOutputKey)('copied_files_count', outputPrefix),
    @@ -494,7 +496,8 @@ const setOutputsAndGetModifiedAndChangedFilesStatus = ({ allDiffFiles, allFilter
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
         yield (0, utils_1.setOutput)({
             key: (0, utils_1.getOutputKey)('modified_files_count', outputPrefix),
    @@ -514,7 +517,8 @@ const setOutputsAndGetModifiedAndChangedFilesStatus = ({ allDiffFiles, allFilter
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
         yield (0, utils_1.setOutput)({
             key: (0, utils_1.getOutputKey)('renamed_files_count', outputPrefix),
    @@ -534,7 +538,8 @@ const setOutputsAndGetModifiedAndChangedFilesStatus = ({ allDiffFiles, allFilter
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
         yield (0, utils_1.setOutput)({
             key: (0, utils_1.getOutputKey)('type_changed_files_count', outputPrefix),
    @@ -554,7 +559,8 @@ const setOutputsAndGetModifiedAndChangedFilesStatus = ({ allDiffFiles, allFilter
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
         yield (0, utils_1.setOutput)({
             key: (0, utils_1.getOutputKey)('unmerged_files_count', outputPrefix),
    @@ -574,7 +580,8 @@ const setOutputsAndGetModifiedAndChangedFilesStatus = ({ allDiffFiles, allFilter
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
         yield (0, utils_1.setOutput)({
             key: (0, utils_1.getOutputKey)('unknown_files_count', outputPrefix),
    @@ -593,7 +600,8 @@ const setOutputsAndGetModifiedAndChangedFilesStatus = ({ allDiffFiles, allFilter
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
         yield (0, utils_1.setOutput)({
             key: (0, utils_1.getOutputKey)('all_changed_and_modified_files_count', outputPrefix),
    @@ -618,7 +626,8 @@ const setOutputsAndGetModifiedAndChangedFilesStatus = ({ allDiffFiles, allFilter
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
         yield (0, utils_1.setOutput)({
             key: (0, utils_1.getOutputKey)('all_changed_files_count', outputPrefix),
    @@ -687,7 +696,8 @@ const setOutputsAndGetModifiedAndChangedFilesStatus = ({ allDiffFiles, allFilter
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
         yield (0, utils_1.setOutput)({
             key: (0, utils_1.getOutputKey)('all_modified_files_count', outputPrefix),
    @@ -768,7 +778,8 @@ const setOutputsAndGetModifiedAndChangedFilesStatus = ({ allDiffFiles, allFilter
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
         yield (0, utils_1.setOutput)({
             key: (0, utils_1.getOutputKey)('deleted_files_count', outputPrefix),
    @@ -1498,6 +1509,7 @@ const getInputs = () => {
         });
         const json = core.getBooleanInput('json', { required: false });
         const escapeJson = core.getBooleanInput('escape_json', { required: false });
    +    const safeOutput = core.getBooleanInput('safe_output', { required: false });
         const fetchDepth = core.getInput('fetch_depth', { required: false });
         const sinceLastRemoteCommit = core.getBooleanInput('since_last_remote_commit', { required: false });
         const writeOutputFiles = core.getBooleanInput('write_output_files', {
    @@ -1587,6 +1599,7 @@ const getInputs = () => {
             dirNamesIncludeFilesSeparator,
             json,
             escapeJson,
    +        safeOutput,
             writeOutputFiles,
             outputDir,
             outputRenamedFilesAsDeletedAndAdded,
    @@ -1752,7 +1765,8 @@ const getChangedFilesFromLocalGitHistory = ({ inputs, env, workingDirectory, fil
                 value: allOldNewRenamedFiles.paths,
                 writeOutputFiles: inputs.writeOutputFiles,
                 outputDir: inputs.outputDir,
    -            json: inputs.json
    +            json: inputs.json,
    +            safeOutput: inputs.safeOutput
             });
             yield (0, utils_1.setOutput)({
                 key: 'all_old_new_renamed_files_count',
    @@ -2800,18 +2814,23 @@ const setArrayOutput = ({ key, inputs, value, outputPrefix }) => __awaiter(void
             writeOutputFiles: inputs.writeOutputFiles,
             outputDir: inputs.outputDir,
             json: inputs.json,
    -        shouldEscape: inputs.escapeJson
    +        shouldEscape: inputs.escapeJson,
    +        safeOutput: inputs.safeOutput
         });
     });
     exports.setArrayOutput = setArrayOutput;
    -const setOutput = ({ key, value, writeOutputFiles, outputDir, json = false, shouldEscape = false }) => __awaiter(void 0, void 0, void 0, function* () {
    +const setOutput = ({ key, value, writeOutputFiles, outputDir, json = false, shouldEscape = false, safeOutput = false }) => __awaiter(void 0, void 0, void 0, function* () {
         let cleanedValue;
         if (json) {
             cleanedValue = (0, exports.jsonOutput)({ value, shouldEscape });
         }
         else {
             cleanedValue = value.toString().trim();
         }
    +    // if safeOutput is true, escape special characters for bash shell
    +    if (safeOutput) {
    +        cleanedValue = cleanedValue.replace(/[^\x20-\x7E]|[:*?"<>|;`$()&!]/g, '\\$&');
    +    }
         core.setOutput(key, cleanedValue);
         if (writeOutputFiles) {
             const extension = json ? 'json' : 'txt';
    
  • dist/index.js.map+1 1 modified
  • README.md+6 0 modified
    @@ -572,6 +572,12 @@ Support this project with a :star:
         # Default: "\n"
         recover_files_separator: ''
     
    +    # Apply sanitization to output filenames before being set as 
    +    # output. 
    +    # Type: boolean
    +    # Default: "true"
    +    safe_output: ''
    +
         # Split character for output strings.
         # Type: string
         # Default: " "
    
  • src/utils.ts+4 1 modified
    @@ -1355,7 +1355,10 @@ export const setOutput = async ({
     
       // if safeOutput is true, escape special characters for bash shell
       if (safeOutput) {
    -    cleanedValue = cleanedValue.replace(/[$()`|&;]/g, '\\$&')
    +    cleanedValue = cleanedValue.replace(
    +      /[^\x20-\x7E]|[:*?"<>|;`$()&!]/g,
    +      '\\$&'
    +    )
       }
     
       core.setOutput(key, cleanedValue)
    
0102c07446a3

Merge pull request from GHSA-mcph-m25j-8j63

6 files changed · +110 29
  • action.yml+4 0 modified
    @@ -134,6 +134,10 @@ inputs:
         description: "Escape JSON output."
         required: false
         default: "true"
    +  safe_output:
    +    description: "Apply sanitization to output filenames before being set as output."
    +    required: false
    +    default: "true"
       fetch_depth:
         description: "Depth of additional branch history fetched. NOTE: This can be adjusted to resolve errors with insufficient history."
         required: false
    
  • README.md+69 15 modified
    @@ -123,14 +123,19 @@ jobs:
           - name: Get changed files
             id: changed-files
             uses: tj-actions/changed-files@v40
    +        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.
             
             # To compare changes between the current commit and the last pushed remote commit set `since_last_remote_commit: true`. e.g
             # with:
             #   since_last_remote_commit: true 
     
           - name: List all changed files
    +        env:
    +          ALL_CHANGED_FILES: |-
    +            ${{ steps.changed-files.outputs.all_changed_files }}
             run: |
    -          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
    +          for file in "$ALL_CHANGED_FILES"; do
                 echo "$file was changed"
               done
     
    @@ -139,14 +144,18 @@ jobs:
             id: changed-markdown-files
             uses: tj-actions/changed-files@v40
             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.
               # Avoid using single or double quotes for multiline patterns
               files: |
                  **.md
     
           - name: List all changed files markdown files
             if: steps.changed-markdown-files.outputs.any_changed == 'true'
    +        env:
    +          ALL_CHANGED_FILES: |-
    +            ${{ steps.changed-markdown-files.outputs.all_changed_files }}
             run: |
    -          for file in ${{ steps.changed-markdown-files.outputs.all_changed_files }}; do
    +          for file in "$ALL_CHANGED_FILES"; do
                 echo "$file was changed"
               done
     
    @@ -155,6 +164,7 @@ jobs:
             id: changed-files-yaml
             uses: tj-actions/changed-files@v40
             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_yaml: |
                 doc:
                   - '**.md'
    @@ -170,29 +180,39 @@ jobs:
           - name: Run step if test file(s) change
             # NOTE: Ensure all outputs are prefixed by the same key used above e.g. `test_(...)` | `doc_(...)` | `src_(...)` when trying to access the `any_changed` output.
             if: steps.changed-files-yaml.outputs.test_any_changed == 'true'  
    +        env:
    +          TEST_ALL_CHANGED_FILES: |-
    +            ${{ steps.changed-files-yaml.outputs.test_all_changed_files }}
             run: |
               echo "One or more test file(s) has changed."
    -          echo "List all the files that have changed: ${{ steps.changed-files-yaml.outputs.test_all_changed_files }}"
    +          echo "List all the files that have changed: $TEST_ALL_CHANGED_FILES"
           
           - name: Run step if doc file(s) change
             if: steps.changed-files-yaml.outputs.doc_any_changed == 'true'
    +        env:
    +          DOC_ALL_CHANGED_FILES: |-
    +            ${{ steps.changed-files-yaml.outputs.doc_all_changed_files }}
             run: |
               echo "One or more doc file(s) has changed."
    -          echo "List all the files that have changed: ${{ steps.changed-files-yaml.outputs.doc_all_changed_files }}"
    +          echo "List all the files that have changed: $DOC_ALL_CHANGED_FILES"
     
           # Example 3
           - name: Get changed files in the docs folder
             id: changed-files-specific
             uses: tj-actions/changed-files@v40
             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: docs/*.{js,html}  # Alternatively using: `docs/**`
               files_ignore: docs/static.js
     
           - name: Run step if any file(s) in the docs folder change
             if: steps.changed-files-specific.outputs.any_changed == 'true'
    +        env:
    +          ALL_CHANGED_FILES: |-
    +            ${{ steps.changed-files-specific.outputs.all_changed_files }}
             run: |
               echo "One or more files in the docs folder has changed."
    -          echo "List all the files that have changed: ${{ steps.changed-files-specific.outputs.all_changed_files }}"
    +          echo "List all the files that have changed: $ALL_CHANGED_FILES"
     ```
     
     #### Using Github's API :octocat:
    @@ -224,10 +244,15 @@ jobs:
           - name: Get changed files
             id: changed-files
             uses: tj-actions/changed-files@v40
    +        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.
     
           - name: List all changed files
    +        env:
    +          ALL_CHANGED_FILES: |-
    +            ${{ steps.changed-files.outputs.all_changed_files }}
             run: |
    -          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
    +          for file in "$ALL_CHANGED_FILES"; do
                 echo "$file was changed"
               done
     ```
    @@ -265,12 +290,17 @@ jobs:
           - name: Get changed files
             id: changed-files
             uses: tj-actions/changed-files@v40
    +        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.
     
           # NOTE: `since_last_remote_commit: true` is implied by default and falls back to the previous local commit.
     
           - name: List all changed files
    +        env:
    +          ALL_CHANGED_FILES: |-
    +            ${{ steps.changed-files.outputs.all_changed_files }}
             run: |
    -          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
    +          for file in "$ALL_CHANGED_FILES"; do
                 echo "$file was changed"
               done
           ...
    @@ -715,10 +745,15 @@ See [inputs](#inputs) for more information.
         - name: Get changed files
           id: changed-files
           uses: tj-actions/changed-files@v40
    +      with:
    +        safe_output: false
     
         - name: List all added files
    +      env:
    +        ADDED_FILES: |-
    +          ${{ steps.changed-files.outputs.added_files }}
           run: |
    -        for file in ${{ steps.changed-files.outputs.added_files }}; do
    +        for file in "$ADDED_FILES"; do
               echo "$file was added"
             done
     ...
    @@ -736,6 +771,8 @@ See [outputs](#outputs) for a list of all available outputs.
         - name: Get changed files
           id: changed-files
           uses: tj-actions/changed-files@v40
    +      with:
    +        safe_output: false
     
         - name: Run a step if my-file.txt was modified
           if: contains(steps.changed-files.outputs.modified_files, 'my-file.txt')
    @@ -756,8 +793,9 @@ See [outputs](#outputs) for a list of all available outputs.
     
        - name: Get changed files and write the outputs to a Txt file
          id: changed-files-write-output-files-txt
    -     uses: ./
    +     uses: tj-actions/changed-files@v40
          with:
    +       safe_output: false
            write_output_files: true
     
        - name: Verify the contents of the .github/outputs/added_files.txt file
    @@ -775,8 +813,9 @@ See [outputs](#outputs) for a list of all available outputs.
     ...
        - name: Get changed files and write the outputs to a JSON file
          id: changed-files-write-output-files-json
    -     uses: ./
    +     uses: tj-actions/changed-files@v40
          with:
    +       safe_output: false
            json: true
            write_output_files: true
     
    @@ -820,6 +859,7 @@ See [inputs](#inputs) for more information.
           id: changed-files-specific
           uses: tj-actions/changed-files@v40
           with:
    +        safe_output: false
             files: |
               my-file.txt
               *.sh
    @@ -840,15 +880,21 @@ See [inputs](#inputs) for more information.
     
         - name: Run step if any of the listed files above is deleted
           if: steps.changed-files-specific.outputs.any_deleted == 'true'
    +      env:
    +        DELETED_FILES: |-
    +          ${{ steps.changed-files-specific.outputs.deleted_files }}
           run: |
    -        for file in ${{ steps.changed-files-specific.outputs.deleted_files }}; do
    +        for file in "$DELETED_FILES"; do
               echo "$file was deleted"
             done
     
         - name: Run step if all listed files above have been deleted
           if: steps.changed-files-specific.outputs.only_deleted == 'true'
    +      env:
    +        DELETED_FILES: |-
    +          ${{ steps.changed-files-specific.outputs.deleted_files }}
           run: |
    -        for file in ${{ steps.changed-files-specific.outputs.deleted_files }}; do
    +        for file in "$DELETED_FILES"; do
               echo "$file was deleted"
             done
     ...
    @@ -958,14 +1004,18 @@ jobs:
             id: changed-files-specific
             uses: tj-actions/changed-files@v40
             with:
    +          safe_output: false
               base_sha: ${{ steps.get-base-sha.outputs.base_sha }}
               files: .github/**
     
           - name: Run step if any file(s) in the .github folder change
             if: steps.changed-files-specific.outputs.any_changed == 'true'
    +        env:
    +          ALL_CHANGED_FILES: |-
    +            ${{ steps.changed-files-specific.outputs.all_changed_files }}
             run: |
               echo "One or more files in the .github folder has changed."
    -          echo "List all the files that have changed: ${{ steps.changed-files-specific.outputs.all_changed_files }}"
    +          echo "List all the files that have changed: $ALL_CHANGED_FILES"
     ...
     ```
     
    @@ -988,11 +1038,15 @@ See [inputs](#inputs) for more information.
           id: changed-files-for-dir1
           uses: tj-actions/changed-files@v40
           with:
    +        safe_output: false
             path: dir1
     
         - name: List all added files in dir1
    +      env:
    +        ADDED_FILES: |-
    +          ${{ steps.changed-files-for-dir1.outputs.added_files }}
           run: |
    -        for file in ${{ steps.changed-files-for-dir1.outputs.added_files }}; do
    +        for file in "$ADDED_FILES"; do
               echo "$file was added"
             done
     ...
    @@ -1015,7 +1069,7 @@ See [inputs](#inputs) for more information.
     
         - name: Run changed-files with quotepath disabled for a specified list of file(s)
           id: changed-files-quotepath-specific
    -      uses: ./
    +      uses: tj-actions/changed-files@v40
           with:
             files: test/test-è.txt
             quotepath: "false"
    
  • src/changedFilesOutput.ts+22 11 modified
    @@ -43,7 +43,8 @@ export const setOutputsAndGetModifiedAndChangedFilesStatus = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
       await setOutput({
         key: getOutputKey('added_files_count', outputPrefix),
    @@ -64,7 +65,8 @@ export const setOutputsAndGetModifiedAndChangedFilesStatus = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
     
       await setOutput({
    @@ -86,7 +88,8 @@ export const setOutputsAndGetModifiedAndChangedFilesStatus = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
     
       await setOutput({
    @@ -108,7 +111,8 @@ export const setOutputsAndGetModifiedAndChangedFilesStatus = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
     
       await setOutput({
    @@ -130,7 +134,8 @@ export const setOutputsAndGetModifiedAndChangedFilesStatus = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
     
       await setOutput({
    @@ -152,7 +157,8 @@ export const setOutputsAndGetModifiedAndChangedFilesStatus = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
     
       await setOutput({
    @@ -174,7 +180,8 @@ export const setOutputsAndGetModifiedAndChangedFilesStatus = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
     
       await setOutput({
    @@ -199,7 +206,8 @@ export const setOutputsAndGetModifiedAndChangedFilesStatus = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
     
       await setOutput({
    @@ -226,7 +234,8 @@ export const setOutputsAndGetModifiedAndChangedFilesStatus = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
     
       await setOutput({
    @@ -314,7 +323,8 @@ export const setOutputsAndGetModifiedAndChangedFilesStatus = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
     
       await setOutput({
    @@ -419,7 +429,8 @@ export const setOutputsAndGetModifiedAndChangedFilesStatus = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
     
       await setOutput({
    
  • src/inputs.ts+3 0 modified
    @@ -34,6 +34,7 @@ export type Inputs = {
       dirNamesDeletedFilesIncludeOnlyDeletedDirs: boolean
       json: boolean
       escapeJson: boolean
    +  safeOutput: boolean
       fetchDepth?: number
       fetchSubmoduleHistory: boolean
       sinceLastRemoteCommit: boolean
    @@ -154,6 +155,7 @@ export const getInputs = (): Inputs => {
       )
       const json = core.getBooleanInput('json', {required: false})
       const escapeJson = core.getBooleanInput('escape_json', {required: false})
    +  const safeOutput = core.getBooleanInput('safe_output', {required: false})
       const fetchDepth = core.getInput('fetch_depth', {required: false})
       const sinceLastRemoteCommit = core.getBooleanInput(
         'since_last_remote_commit',
    @@ -272,6 +274,7 @@ export const getInputs = (): Inputs => {
         dirNamesIncludeFilesSeparator,
         json,
         escapeJson,
    +    safeOutput,
         writeOutputFiles,
         outputDir,
         outputRenamedFilesAsDeletedAndAdded,
    
  • src/main.ts+2 1 modified
    @@ -173,7 +173,8 @@ const getChangedFilesFromLocalGitHistory = async ({
           value: allOldNewRenamedFiles.paths,
           writeOutputFiles: inputs.writeOutputFiles,
           outputDir: inputs.outputDir,
    -      json: inputs.json
    +      json: inputs.json,
    +      safeOutput: inputs.safeOutput
         })
         await setOutput({
           key: 'all_old_new_renamed_files_count',
    
  • src/utils.ts+10 2 modified
    @@ -1324,7 +1324,8 @@ export const setArrayOutput = async ({
         writeOutputFiles: inputs.writeOutputFiles,
         outputDir: inputs.outputDir,
         json: inputs.json,
    -    shouldEscape: inputs.escapeJson
    +    shouldEscape: inputs.escapeJson,
    +    safeOutput: inputs.safeOutput
       })
     }
     
    @@ -1334,14 +1335,16 @@ export const setOutput = async ({
       writeOutputFiles,
       outputDir,
       json = false,
    -  shouldEscape = false
    +  shouldEscape = false,
    +  safeOutput = false
     }: {
       key: string
       value: string | string[] | boolean
       writeOutputFiles: boolean
       outputDir: string
       json?: boolean
       shouldEscape?: boolean
    +  safeOutput?: boolean
     }): Promise<void> => {
       let cleanedValue
       if (json) {
    @@ -1350,6 +1353,11 @@ export const setOutput = async ({
         cleanedValue = value.toString().trim()
       }
     
    +  // if safeOutput is true, escape special characters for bash shell
    +  if (safeOutput) {
    +    cleanedValue = cleanedValue.replace(/[$()`|&;]/g, '\\$&')
    +  }
    +
       core.setOutput(key, cleanedValue)
     
       if (writeOutputFiles) {
    

Vulnerability mechanics

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

References

6

News mentions

0

No linked articles in our index yet.