tj-actions/changed-files command injection in output filenames
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]
- GitHub - tj-actions/changed-files: :octocat: Github action to retrieve all (added, copied, modified, deleted, renamed, type changed, unmerged, unknown) files and directories.
- Merge pull request from GHSA-mcph-m25j-8j63 · tj-actions/changed-files@0102c07
- NVD - CVE-2023-51664
- Potential Actions command injection in output filenames (GHSL-2023-271)
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.
| Package | Affected versions | Patched versions |
|---|---|---|
tj-actions/changed-filesGitHub Actions | < 41 | 41 |
Affected products
2- Range: < 41.0.0
Patches
3716b1e130428fix: update characters escaped by safe output (#1815)
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 modifiedsrc/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)
ff2f6e6b9191fix: update safe output regex and the docs (#1805)
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 modifiedREADME.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)
0102c07446a3Merge 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- github.com/advisories/GHSA-mcph-m25j-8j63ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-51664ghsaADVISORY
- github.com/tj-actions/changed-files/commit/0102c07446a3cad972f4afcbd0ee4dbc4b6d2d1bghsax_refsource_MISCWEB
- github.com/tj-actions/changed-files/commit/716b1e13042866565e00e85fd4ec490e186c4a2fghsax_refsource_MISCWEB
- github.com/tj-actions/changed-files/commit/ff2f6e6b91913a7be42be1b5917330fe442f2edeghsax_refsource_MISCWEB
- github.com/tj-actions/changed-files/security/advisories/GHSA-mcph-m25j-8j63ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.