Compare commits

...

4 Commits

Author SHA1 Message Date
Michal Dorner
ceb8a2b8f2 Update CHANGELOG.md for v4.0.3 and v3.0.4 (#327) 2026-08-05 14:05:15 +02:00
Michal Dorner
ef09b88f3e Document safe handling of file list outputs in workflows (#326)
Recommend passing *_files values through env: instead of interpolating them directly into run: scripts, and update README examples and CI workflows to follow that pattern.

Credits: https://github.com/tjswlsgg
2026-08-05 13:45:25 +02:00
Michal Dorner
44adc5b06d Merge commit from fork
fix: escape multi-line filenames in list-files shell and csv output
2026-08-05 12:02:19 +02:00
Ken Matsui
93c889f9e5 fix: escape multi-line filenames in list-files shell and csv output
Fixes GHSA-7hc6-8hq5-9q2m
2026-07-30 23:48:19 -04:00
8 changed files with 61 additions and 12 deletions

View File

@@ -193,7 +193,9 @@ jobs:
excludesOnly:
- '!**/*.md'
- name: Print 'mobile_files'
run: echo ${{steps.filter.outputs.mobile_files}}
env:
MOBILE_FILES: ${{ steps.filter.outputs.mobile_files }}
run: echo "$MOBILE_FILES"
- name: filter-test
if: |
steps.filter.outputs.mobile != 'true'
@@ -227,11 +229,17 @@ jobs:
any:
- added|deleted|modified: "*"
- name: Print 'added_files'
run: echo ${{steps.filter.outputs.added_files}}
env:
ADDED_FILES: ${{ steps.filter.outputs.added_files }}
run: echo "$ADDED_FILES"
- name: Print 'modified_files'
run: echo ${{steps.filter.outputs.modified_files}}
env:
MODIFIED_FILES: ${{ steps.filter.outputs.modified_files }}
run: echo "$MODIFIED_FILES"
- name: Print 'deleted_files'
run: echo ${{steps.filter.outputs.deleted_files}}
env:
DELETED_FILES: ${{ steps.filter.outputs.deleted_files }}
run: echo "$DELETED_FILES"
- name: filter-test
if: |
steps.filter.outputs.added != 'true'

View File

@@ -1,5 +1,13 @@
# Changelog
## v4.0.3
- [Document safe handling of file list outputs in workflows](https://github.com/dorny/paths-filter/pull/326)
- [Escape multi-line filenames in list-files shell and csv output](https://github.com/advisories/GHSA-7hc6-8hq5-9q2m)
- [Add 'some-with-excludes' predicate quantifier](https://github.com/dorny/paths-filter/pull/322)
- [Add contents permission to PR example](https://github.com/dorny/paths-filter/pull/248)
- [Scope base-ignored warning to API path](https://github.com/dorny/paths-filter/pull/319)
- [Update outputs in readme to account for the 'every' predicate-quantifier](https://github.com/dorny/paths-filter/pull/247)
## v4.0.2
- [Work around git dubious ownership errors in container jobs](https://github.com/dorny/paths-filter/pull/317)
- [Use rev-parse instead of branch --show-current for older git compat](https://github.com/dorny/paths-filter/pull/303)
@@ -11,6 +19,9 @@
## v4.0.0
- [Update action runtime to node24](https://github.com/dorny/paths-filter/pull/294)
## v3.0.4
- [Escape multi-line filenames in list-files shell and csv output](https://github.com/advisories/GHSA-7hc6-8hq5-9q2m)
## v3.0.3
- [Add missing predicate-quantifier](https://github.com/dorny/paths-filter/pull/279)

View File

@@ -67,6 +67,10 @@ For more scenarios see [examples](#examples) section.
## Notes
- **Security:** `${FILTER_NAME}_files` outputs contain filenames that may be attacker-influenced on pull requests.
Do not interpolate them directly into a `run:` script with `${{ ... }}`.
Pass the value through `env:` and reference the variable from the shell instead.
See [Custom processing of changed files](#custom-processing-of-changed-files).
- Paths expressions are evaluated using [picomatch](https://github.com/micromatch/picomatch) library.
Documentation for path expression format can be found on the project GitHub page.
- Picomatch [dot](https://github.com/micromatch/picomatch#options) option is set to true.
@@ -205,7 +209,7 @@ For more information, see [CHANGELOG](https://github.com/dorny/paths-filter/blob
- `'true'` - if **any** changed file matches **at least one** of the filter's rules and **none** of its negated rules
- `'false'` - if **no** changed file matches **at least one** of the filter's rules and **none** of its negated rules
- Each filter sets an output variable with the name `${FILTER_NAME}_count` to the count of matching files.
- If enabled, for each filter it sets an output variable with the name `${FILTER_NAME}_files`. It will contain a list of all files matching the filter.
- If enabled, for each filter it sets an output variable with the name `${FILTER_NAME}_files`. It will contain a list of all files matching the filter. Treat these values as untrusted when filenames can come from pull requests.
- `changes` - JSON array with names of all filters matching any of the changed files.
## Examples
@@ -589,9 +593,13 @@ jobs:
- added|modified: '*.md'
- name: Lint Markdown
if: ${{ steps.filter.outputs.markdown == 'true' }}
run: npx textlint ${{ steps.filter.outputs.markdown_files }}
env:
MARKDOWN_FILES: ${{ steps.filter.outputs.markdown_files }}
run: npx textlint $MARKDOWN_FILES
```
When passing file lists to shell commands, use `env:` as shown above. Do not write `${{ steps.filter.outputs.markdown_files }}` directly inside the `run:` script.
</details>
<details>
@@ -617,6 +625,8 @@ jobs:
files: ${{ steps.filter.outputs.changed_files }}
```
The `json` and `csv` formats are intended as structured data for scripts, programs, or other actions. Passing them to an action input as above is fine. Do not interpolate `json` or `csv` outputs directly into a `run:` script.
</details>
## See also

View File

@@ -20,4 +20,12 @@ describe('csvEscape() backslash escapes every character except subset of definit
test('Double quote should be escaped by another double quote', () => {
expect(csvEscape('file " with double quote')).toBe('"file "" with double quote"')
})
test('filename with LF should be quoted per RFC 4180', () => {
expect(csvEscape('a\nb')).toBe('"a\nb"')
})
test('filename with CRLF should be quoted per RFC 4180', () => {
expect(csvEscape('a\r\nb')).toBe('"a\r\nb"')
})
})

View File

@@ -54,4 +54,16 @@ describe('shellEscape() returns human readable filenames with as few escaping ap
test('filename with single quote and special characters is split and quoted/escaped as needed', () => {
expect(shellEscape("file'with $quote")).toBe("file\\''with $quote'")
})
test('filename with LF should be single-quoted', () => {
expect(shellEscape('x\ntouch pwned.md')).toBe("'x\ntouch pwned.md'")
})
test('filename with CRLF should be single-quoted', () => {
expect(shellEscape('x\r\ntouch pwned.md')).toBe("'x\r\ntouch pwned.md'")
})
test('filename with CR should be single-quoted', () => {
expect(shellEscape('a\rb')).toBe("'a\rb'")
})
})

6
dist/index.js vendored
View File

@@ -547,7 +547,7 @@ function csvEscape(value) {
if (value === '')
return value;
// Only safe characters
if (/^[a-zA-Z0-9._+:@%/-]+$/m.test(value)) {
if (/^[a-zA-Z0-9._+:@%/-]+$/.test(value)) {
return value;
}
// https://tools.ietf.org/html/rfc4180
@@ -579,12 +579,12 @@ function shellEscape(value) {
if (value === '')
return value;
// Only safe characters
if (/^[a-zA-Z0-9,._+:@%/-]+$/m.test(value)) {
if (/^[a-zA-Z0-9,._+:@%/-]+$/.test(value)) {
return value;
}
if (value.includes("'")) {
// Only safe characters, single quotes and white-spaces
if (/^[a-zA-Z0-9,._+:@%/'\s-]+$/m.test(value)) {
if (/^[a-zA-Z0-9,._+:@%/'\s-]+$/.test(value)) {
return `"${value}"`;
}
// Split by single quote and apply escaping recursively

View File

@@ -4,7 +4,7 @@ export function csvEscape(value: string): string {
if (value === '') return value
// Only safe characters
if (/^[a-zA-Z0-9._+:@%/-]+$/m.test(value)) {
if (/^[a-zA-Z0-9._+:@%/-]+$/.test(value)) {
return value
}

View File

@@ -9,13 +9,13 @@ export function shellEscape(value: string): string {
if (value === '') return value
// Only safe characters
if (/^[a-zA-Z0-9,._+:@%/-]+$/m.test(value)) {
if (/^[a-zA-Z0-9,._+:@%/-]+$/.test(value)) {
return value
}
if (value.includes("'")) {
// Only safe characters, single quotes and white-spaces
if (/^[a-zA-Z0-9,._+:@%/'\s-]+$/m.test(value)) {
if (/^[a-zA-Z0-9,._+:@%/'\s-]+$/.test(value)) {
return `"${value}"`
}