-
Notifications
You must be signed in to change notification settings - Fork 70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pass inputs to env before reading #66
Changes from all commits
27a08b3
7bc2356
046cc62
1abc20e
9efe230
1b56179
f93c360
3c8c1f0
f2b0110
5e9d5c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,22 +58,26 @@ runs: | |
steps: | ||
- name: Enable problem-matcher | ||
shell: bash | ||
env: | ||
format: ${{ inputs.format }} | ||
disable_matcher: ${{ inputs.disable_matcher }} | ||
run: | | ||
problem_matcher_file="${{ github.action_path }}/.github/problem-matcher-${{ inputs.format }}.json" | ||
if [[ ${{ inputs.disable_matcher }} != "true" && -f "$problem_matcher_file" ]]; then | ||
problem_matcher_file="${{ github.action_path }}/.github/problem-matcher-${format}.json" | ||
if [[ "${disable_matcher}" != "true" && -f "$problem_matcher_file" ]]; then | ||
echo "::add-matcher::$problem_matcher_file" | ||
fi | ||
|
||
- name: Download shellcheck | ||
shell: bash | ||
env: | ||
scversion: ${{ inputs.version }} | ||
run: | | ||
if [[ "${{ runner.os }}" == "macOS" ]]; then | ||
osvariant="darwin" | ||
else | ||
osvariant="linux" | ||
fi | ||
|
||
scversion="${{ inputs.version }}" | ||
baseurl="https://github.com/koalaman/shellcheck/releases/download" | ||
|
||
curl -Lso "${{ github.action_path }}/sc.tar.xz" \ | ||
|
@@ -91,42 +95,49 @@ runs: | |
- name: Set options | ||
shell: bash | ||
id: options | ||
env: | ||
severity: ${{ inputs.severity }} | ||
format: ${{ inputs.format }} | ||
run: | | ||
declare -a options | ||
if [[ -n "${{ inputs.severity }}" ]]; then | ||
options+=("-S ${{ inputs.severity }}") | ||
if [[ -n "${severity}" ]]; then | ||
options+=("-S ${severity}") | ||
fi | ||
options+=("--format=${{ inputs.format }}") | ||
options+=("--format=${format}") | ||
echo "options=${options[@]}" >> $GITHUB_OUTPUT | ||
|
||
- name: Gather excluded paths | ||
shell: bash | ||
id: exclude | ||
env: | ||
ignore: ${{ inputs.ignore }} | ||
ignore_paths: ${{ inputs.ignore_paths }} | ||
ignore_names: ${{ inputs.ignore_names }} | ||
run: | | ||
declare -a excludes | ||
set -f # temporarily disable globbing so that globs in input aren't expanded | ||
|
||
excludes+=("! -path \"*./.git/*\"") | ||
excludes+=("! -path \"*.go\"") | ||
excludes+=("! -path \"*/mvnw\"") | ||
if [[ -n "${{ inputs.ignore }}" ]]; then | ||
excludes+=("! -path *./.git/*") | ||
excludes+=("! -path *.go") | ||
excludes+=("! -path */mvnw") | ||
if [[ -n "${ignore}" ]]; then | ||
echo "::warning::ignore is deprecated. Please use ignore_paths instead" | ||
for path in ${{ inputs.ignore }}; do | ||
for path in ${ignore}; do | ||
echo "::debug:: Adding '$path' to excludes" | ||
excludes+=("! -path \"*./$path/*\"") | ||
excludes+=("! -path \"*/$path/*\"") | ||
excludes+=("! -path \"$path\"") | ||
excludes+=("! -path *./$path/*") | ||
excludes+=("! -path */$path/*") | ||
excludes+=("! -path $path") | ||
done | ||
else | ||
for path in ${{ inputs.ignore_paths }}; do | ||
for path in ${ignore_paths}; do | ||
echo "::debug:: Adding '$path' to excludes" | ||
excludes+=("! -path \"*./$path/*\"") | ||
excludes+=("! -path \"*/$path/*\"") | ||
excludes+=("! -path \"$path\"") | ||
excludes+=("! -path *./$path/*") | ||
excludes+=("! -path */$path/*") | ||
excludes+=("! -path $path") | ||
done | ||
fi | ||
|
||
for name in ${{ inputs.ignore_names }}; do | ||
for name in ${ignore_names}; do | ||
echo "::debug:: Adding '$name' to excludes" | ||
excludes+=("! -name $name") | ||
done | ||
|
@@ -137,26 +148,36 @@ runs: | |
- name: Gather additional files | ||
shell: bash | ||
id: additional | ||
env: | ||
additional_files: ${{ inputs.additional_files }} | ||
run: | | ||
declare -a files | ||
for file in ${{ inputs.additional_files }}; do | ||
echo "::debug:: Adding '$file' to excludes" | ||
files+=("-o -name \"*$file\"") | ||
for file in ${additional_files}; do | ||
echo "::debug:: Adding '$file' to additional files" | ||
files+=("-o -name *$file") | ||
done | ||
echo "files=${files[@]}" >> $GITHUB_OUTPUT | ||
|
||
- name: Run the check | ||
shell: bash | ||
id: check | ||
env: | ||
scandir: ${{ inputs.scandir }} | ||
check_together: ${{ inputs.check_together }} | ||
exclude_args: ${{ steps.exclude.outputs.excludes }} | ||
additional_file_args: ${{ steps.additional.outputs.files }} | ||
Comment on lines
+167
to
+168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to change outputs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that this depends on the output. For example, in the case of If someone adds a malicious payload into one of those inputs, it can eventually executed when those outputs are injected. Some of the outputs aren't function of inputs so they could be reverted back but I wanted to be safe. This ensures that scripts can't be injected with reflection. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They do not? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was referring to the hypothetical case where the code would use an intermediate env var for the input but not the output. I believe that's what you're suggesting here. My understanding is that you're saying that outputs don't need to pass through intermediate variables. I believe that they do need to passed through an intermediate variable. Here's simplified snippet of what I mean. I'll take the # ...
- name: Set options
shell: bash
id: options
env:
# Note: Inputs passed through intermediate env vars
severity: ${{ inputs.severity }}
format: ${{ inputs.format }}
run: |
declare -a options
if [[ -n "${severity}" ]]; then
options+=("-S ${severity}")
fi
options+=("--format=${format}")
echo "::set-output name=options::${options[@]}"
# ...
- name: Print information
shell: bash
# Note: Outputs passed as is with no intermediate variable as you're suggesting
run: |
echo "Files: ${{steps.filepaths.outputs.filepaths}}"
echo "Excluded: ${{ steps.exclude.outputs.excludes }}"
echo "Options: ${{ steps.options.outputs.options }}"
echo "Status code: ${{steps.check.outputs.statuscode}}"
exit ${{steps.check.outputs.statuscode}} Getting code execution in this context can be done by calling the action like this: use: ludeeus/action-shellcheck@master
with:
format: 'gcc";echo "injected code";echo "' The injection would happen on the following line: echo "Options: ${{ steps.options.outputs.options }}" which would be interpreted as echo "Options: --format=";echo "injected code"; echo "" This example is theoretical as I haven't tested it. It may require an extra quote or backslash here and there. I hope that illustrates my point that outputs that may contain user inputs need to be passed through intermediate env variables as well. For the protection against this kind of attack to work, intermediate variables need to be used for every value that may have included user input. |
||
shellcheck_options: ${{ steps.options.outputs.options }} | ||
run: | | ||
statuscode=0 | ||
declare -a filepaths | ||
shebangregex="^#! */[^ ]*/(env *)?[abk]*sh" | ||
|
||
set -f # temporarily disable globbing so that globs in inputs aren't expanded | ||
|
||
while IFS= read -r -d '' file; do | ||
filepaths+=("$file") | ||
done < <(find "${{ inputs.scandir }}" \ | ||
${{ steps.exclude.outputs.excludes }} \ | ||
done < <(find "${scandir}" \ | ||
${exclude_args} \ | ||
-type f \ | ||
'(' \ | ||
-name '*.bash' \ | ||
|
@@ -185,34 +206,36 @@ runs: | |
-o -path '*/.profile' \ | ||
-o -path '*/profile' \ | ||
-o -name '*.shlib' \ | ||
${{ steps.additional.outputs.files }} \ | ||
${additional_file_args} \ | ||
')' \ | ||
-print0) | ||
|
||
while IFS= read -r -d '' file; do | ||
head -n1 "$file" | grep -Eqs "$shebangregex" || continue | ||
filepaths+=("$file") | ||
done < <(find "${{ inputs.scandir }}" \ | ||
${{ steps.exclude.outputs.excludes }} \ | ||
done < <(find "${scandir}" \ | ||
${exclude_args} \ | ||
-type f ! -name '*.*' -perm /111 \ | ||
-print0) | ||
|
||
if [[ -n "${{ inputs.check_together }}" ]]; then | ||
if [[ -n "${check_together}" ]]; then | ||
"${{ github.action_path }}/shellcheck" \ | ||
${{ steps.options.outputs.options }} \ | ||
${shellcheck_options} \ | ||
"${filepaths[@]}" || statuscode=$? | ||
else | ||
for file in "${filepaths[@]}"; do | ||
echo "::debug::Checking '$file'" | ||
"${{ github.action_path }}/shellcheck" \ | ||
${{ steps.options.outputs.options }} \ | ||
${shellcheck_options} \ | ||
"$file" || statuscode=$? | ||
done | ||
fi | ||
|
||
echo "filepaths=${filepaths[@]}" >> $GITHUB_OUTPUT | ||
echo "statuscode=$statuscode" >> $GITHUB_OUTPUT | ||
|
||
set +f # re-enable globbing | ||
|
||
- name: Exit action | ||
shell: bash | ||
run: exit ${{steps.check.outputs.statuscode}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR 👍
There are no problems here with the expected usage of the inputs, you really need to go out of your way for this to be a problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally believe that most people will pass in hard-coded strings to this action so they won't be vulnerable. I think that people can be very creative when it comes to their CI and GitHub actions. I don't them to be accidentally vulnerable to script injection issues.
Also, in it's security recommendations, GH states that it's safe to pass untrusted values to actions is safe: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-action-instead-of-an-inline-script-recommended
With that in mind, I think that it's fair to think that people will pass in untrusted data into this action.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point was that none of the inputs for this action make sense to have a dynamic based on PR/commit title/content, which is what the goal of this PR was.
BTW that recommendation is laughable, that is not true at all...
GitHub even hosted a CTF around it last year.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One example that comes to mind is that someone might want to expose the
severity
input in such a way that someone writing a PR may opt-into runningshellcheck
in a more aggressive mode. This could be exposed by having a previous step read the PR title or description to extract a special value that is then passed into this action. For example, a previous step could look forforce_shellcheck_severity=...
in the description.Another simpler approach would be to create a manual workflow that exposes the
severity
option as an input. This workflow could be used by someone who wants to allow devs to run shellcheck in a more aggressive mode in special cases.These theoretical examples are likely not going to get implemented often but I think that there's reasonable enough that someone may want to something like that. In which case, they would be vulnerable to a script injection attack through this action.
Perhaps it's a question of philosophy. In my mind, when I use an action or a library, I expect it to be secure by default and to treat all inputs as untrusted. For example, if someone passes
$(echo hi)
as thescandir
input, I expect it to look into a directory named$(echo hi)
and fail. I don't expect it to actually runecho hi
. Here I'm framing this as a security issue but it could also be seen as a regular bug because$(echo hi)
is technically a valid directory name and it's not being handled correctly. The example here is convoluted but it's not completely impossible for someone to use$(...)
somewhere in a directory name.