sec: harden workflow files against shell injection and over-privileged tokens#2046
Conversation
…d tokens
- Add validate-inputs jobs to npm-publish, docker-manual, and manual-release
workflows; user-supplied inputs are checked against strict patterns before
any step that touches secrets or publishes artefacts.
- Replace every ${{ inputs.* }} / ${{ env.* }} / ${{ matrix.* }} interpolation
inside run: blocks with env: block assignments + plain shell variable
references, eliminating the script-injection vector where a crafted dispatch
input could execute arbitrary commands.
- Add explicit permissions: contents: read blocks to chart-ci, frontend-ci,
npm-publish-test, and release workflows so the default broad GITHUB_TOKEN
scope is not silently inherited.
- Set permissions: contents: write on release and manual-release (minimum
needed for GoReleaser to create releases and upload assets).
https://claude.ai/code/session_01RRgwJRqQD73fFsvYexKm79
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughUpdates seven GitHub Actions workflow files to add explicit permission configurations restricting default token access, introduce input validation gates that reject malformed values before downstream operations, and refactor shell scripts to pass GitHub Actions template values through environment variables instead of direct interpolation. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
.github/workflows/npm-publish.yaml (2)
170-184:⚠️ Potential issue | 🟡 MinorMissed interpolation:
cd npm/${{ matrix.package }}still insiderun:.The PR description states all
${{ matrix.* }}interpolations insiderun:blocks were replaced. This site (and${{ matrix.package }}quoting incontinue-on-error: ${{ matrix.arch == ... }}is fine since that's not inrun:) was missed.matrix.packageis statically defined so it's not currently exploitable, but the inconsistency makes the file's hardening pattern fragile to future matrix additions.♻️ Proposed change
- name: Publish to NPM + env: + PACKAGE_NAME: ${{ matrix.package }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | - cd npm/${{ matrix.package }} + cd "npm/${PACKAGE_NAME}" # Try to publish, but don't fail if version already exists npm publish --access public --provenance 2>&1 | tee publish.log || { if grep -q "cannot publish over the previously published versions" publish.log; then echo "Version already published, skipping..." exit 0 else exit 1 fi } - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} continue-on-error: ${{ matrix.arch == 'ppc64' || matrix.arch == 's390x' || matrix.arch == 'armv6' }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/npm-publish.yaml around lines 170 - 184, Replace the direct GitHub Actions expression inside the run block for the "Publish to NPM" step: define an env variable (e.g. PACKAGE: ${{ matrix.package }}) on that step and use shell variable expansion in the run commands (cd npm/$PACKAGE) instead of cd npm/${{ matrix.package }}; update the run block in the "Publish to NPM" step to reference $PACKAGE and leave the existing continue-on-error expression as-is.
187-211:⚠️ Potential issue | 🟠 Major
validate-inputscan be bypassed forpublish-main-packagedue toif: always().Chain:
validate-inputs → publish-platform-packages → publish-main-package. Withneeds: publish-platform-packages+if: always(), whenvalidate-inputsfails,publish-platform-packagesis skipped, butpublish-main-packagestill runs becausealways()evaluates true regardless of upstreamskippedstatus.
publish-main-packagethen proceeds with the unvalidated workflow-levelVERSIONenv (computed from rawinputs.version) and exposesNODE_AUTH_TOKEN(line 250) to a step that runsnpm version "$PKG_VERSION"andnpm publish. Quoting prevents RCE, but the regex gate is rendered ineffective for the most secret-sensitive job in the workflow.🔒️ Proposed fix
publish-main-package: - needs: publish-platform-packages - if: always() + needs: [validate-inputs, publish-platform-packages] + if: always() && needs.validate-inputs.result == 'success' runs-on: ubuntu-latestApply the same hardening to
verify-packages(line 254) so it also short-circuits on validation failure:verify-packages: - needs: [publish-platform-packages, publish-main-package] - if: always() + needs: [validate-inputs, publish-platform-packages, publish-main-package] + if: always() && needs.validate-inputs.result == 'success'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/npm-publish.yaml around lines 187 - 211, The publish-main-package job currently uses if: always() which allows it to run even when the upstream validate-inputs job failed; update the job so it only runs when validation succeeded by changing the job's needs to include validate-inputs (e.g., needs: [publish-platform-packages, validate-inputs]) and replace if: always() with a conditional that requires both upstream jobs to succeed (e.g., if: ${{ needs.publish-platform-packages.result == 'success' && needs.validate-inputs.result == 'success' }}); also apply the same pattern to the verify-packages step so it short-circuits on validation failure.
🧹 Nitpick comments (3)
.github/workflows/npm-publish.yaml (1)
33-33: Empty-string guard is dead code underworkflow_dispatch.
inputs.versionisrequired: true(line 8), so[ -n "$INPUT_VERSION" ]is always true at this trigger. The||fallback at line 12 (github.event.release.tag_name || inputs.version) suggests this workflow once had arelease:trigger that has since been removed. Either re-add the trigger or drop the deadRELEASE_TAG_NAMEplumbing in a follow-up — not blocking.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/npm-publish.yaml at line 33, The branch contains dead-code guarding INPUT_VERSION with [ -n "$INPUT_VERSION" ] because workflow_dispatch declares inputs.version as required and INPUT_VERSION is always set; update the action to either remove the redundant empty-string check (delete the [ -n "$INPUT_VERSION" ] && prefix) or restore the prior release trigger and RELEASE_TAG_NAME plumbing so the conditional makes sense; locate the conditional using INPUT_VERSION and the expression github.event.release.tag_name || inputs.version and apply one of those two fixes consistently across the workflow..github/workflows/docker-manual.yaml (2)
58-73: LGTM —casematching against$INPUT_REFis the right pattern.The same env→shell-var rewrite repeated identically in
docker-default,docker-dev, anddocker-alpine(lines 58–73, 112–125, 165–178). Since the three jobs are otherwise close to identical aside from Dockerfile/tag suffix, consider extracting to a composite action or reusable workflow in a follow-up — purely a maintenance win, not blocking.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/docker-manual.yaml around lines 58 - 73, The "Resolve version" step (id: get_version) that sets INPUT_REF -> VERSION via the case block is duplicated across jobs docker-default, docker-dev, and docker-alpine; extract that logic into a single reusable unit (either a composite action or a reusable workflow) and replace the duplicated steps in each job with a call to that unit, ensuring the composite accepts INPUT_REF (or inputs.ref) and exposes VERSION via outputs or writes to GITHUB_ENV exactly as the current step does to preserve behavior.
15-31: Prefer an allow-list for git refs over a deny-list of metacharacters.The current deny-list
[[:space:];|&$\()<>!]misses several shell-significant characters (`,',",*,?,{,},~,#). It currently doesn't matter becauseINPUT_REFis passed viaenv:and double-quoted incase "$INPUT_REF", but a future refactor that drops the env-passing would silently re-open injection.Git refs are constrained by
git check-ref-format, so an allow-list is both stricter and easier to reason about. Also: empty check should come first to short-circuit.🛡️ Suggested allow-list
- name: Validate ref input env: # Pass via env so the value is treated as data, not interpolated code. INPUT_REF: ${{ inputs.ref }} run: | - # Reject refs containing shell metacharacters (space, ;, &, |, $, (, ), `, <, >, !). - if echo "$INPUT_REF" | grep -qE '[[:space:];|&$`()<>!]'; then - echo "::error::inputs.ref contains invalid characters: ${INPUT_REF}" - exit 1 - fi if [ -z "$INPUT_REF" ]; then echo "::error::inputs.ref must not be empty" exit 1 fi + # Allow only characters valid in git refs / version tags. + if ! printf '%s' "$INPUT_REF" | grep -qE '^(refs/(heads|tags)/)?[A-Za-z0-9._/-]+$'; then + echo "::error::inputs.ref must be a valid git ref (got: ${INPUT_REF})" + exit 1 + fi🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/docker-manual.yaml around lines 15 - 31, Replace the deny-list metacharacter check in the validate-inputs step with an allow-list that enforces Git ref syntax and move the emptiness check first: first test if INPUT_REF is empty and fail, then validate INPUT_REF against a strict allow-regex equivalent to git check-ref-format (e.g. allow only characters and patterns permitted in refs: no ASCII control chars, no consecutive dots, no component starting/ending with dot, no components with @{-}, no space, and only the standard safe characters), failing with a clear "::error::" message if the ref does not match; update the script that references INPUT_REF in the validate-inputs job so the condition uses this allow-list regex and preserve quoting of "$INPUT_REF" when logging errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/docker-manual.yaml:
- Around line 15-17: Add an explicit job-level permissions block to the
validate-inputs job to drop the runner token scope; update the validate-inputs
job (named "validate-inputs") to include a permissions: none entry so the job
runs without any GitHub token privileges since it only validates a string and
exits.
In @.github/workflows/npm-publish.yaml:
- Around line 23-36: The validate-inputs job currently inherits workflow-level
id-token: write; modify the validate-inputs job (the job named validate-inputs)
to explicitly restrict permissions by adding a permissions block that sets
id-token: none so it cannot mint OIDC tokens (keep other workflow permissions
unchanged); update the job definition to include that permissions stanza
directly under the validate-inputs job header to override the workflow-level
permission.
---
Outside diff comments:
In @.github/workflows/npm-publish.yaml:
- Around line 170-184: Replace the direct GitHub Actions expression inside the
run block for the "Publish to NPM" step: define an env variable (e.g. PACKAGE:
${{ matrix.package }}) on that step and use shell variable expansion in the run
commands (cd npm/$PACKAGE) instead of cd npm/${{ matrix.package }}; update the
run block in the "Publish to NPM" step to reference $PACKAGE and leave the
existing continue-on-error expression as-is.
- Around line 187-211: The publish-main-package job currently uses if: always()
which allows it to run even when the upstream validate-inputs job failed; update
the job so it only runs when validation succeeded by changing the job's needs to
include validate-inputs (e.g., needs: [publish-platform-packages,
validate-inputs]) and replace if: always() with a conditional that requires both
upstream jobs to succeed (e.g., if: ${{ needs.publish-platform-packages.result
== 'success' && needs.validate-inputs.result == 'success' }}); also apply the
same pattern to the verify-packages step so it short-circuits on validation
failure.
---
Nitpick comments:
In @.github/workflows/docker-manual.yaml:
- Around line 58-73: The "Resolve version" step (id: get_version) that sets
INPUT_REF -> VERSION via the case block is duplicated across jobs
docker-default, docker-dev, and docker-alpine; extract that logic into a single
reusable unit (either a composite action or a reusable workflow) and replace the
duplicated steps in each job with a call to that unit, ensuring the composite
accepts INPUT_REF (or inputs.ref) and exposes VERSION via outputs or writes to
GITHUB_ENV exactly as the current step does to preserve behavior.
- Around line 15-31: Replace the deny-list metacharacter check in the
validate-inputs step with an allow-list that enforces Git ref syntax and move
the emptiness check first: first test if INPUT_REF is empty and fail, then
validate INPUT_REF against a strict allow-regex equivalent to git
check-ref-format (e.g. allow only characters and patterns permitted in refs: no
ASCII control chars, no consecutive dots, no component starting/ending with dot,
no components with @{-}, no space, and only the standard safe characters),
failing with a clear "::error::" message if the ref does not match; update the
script that references INPUT_REF in the validate-inputs job so the condition
uses this allow-list regex and preserve quoting of "$INPUT_REF" when logging
errors.
In @.github/workflows/npm-publish.yaml:
- Line 33: The branch contains dead-code guarding INPUT_VERSION with [ -n
"$INPUT_VERSION" ] because workflow_dispatch declares inputs.version as required
and INPUT_VERSION is always set; update the action to either remove the
redundant empty-string check (delete the [ -n "$INPUT_VERSION" ] && prefix) or
restore the prior release trigger and RELEASE_TAG_NAME plumbing so the
conditional makes sense; locate the conditional using INPUT_VERSION and the
expression github.event.release.tag_name || inputs.version and apply one of
those two fixes consistently across the workflow.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 033d9801-74f7-4039-a297-f600e82bac0a
📒 Files selected for processing (7)
.github/workflows/chart-ci.yaml.github/workflows/docker-manual.yaml.github/workflows/frontend-ci.yaml.github/workflows/manual-release.yaml.github/workflows/npm-publish-test.yaml.github/workflows/npm-publish.yaml.github/workflows/release.yaml
| validate-inputs: | ||
| runs-on: ubuntu-latest | ||
| steps: |
There was a problem hiding this comment.
Add an explicit permissions: to the validation job.
This workflow has no workflow-level permissions: block, so validate-inputs runs with the runner default — which can be permissive depending on repo/org settings. Since this job only validates a string and exits, it should drop all token scope explicitly.
🛡️ Proposed change
validate-inputs:
runs-on: ubuntu-latest
+ permissions: {}
steps:
- name: Validate ref input📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| validate-inputs: | |
| runs-on: ubuntu-latest | |
| steps: | |
| validate-inputs: | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/docker-manual.yaml around lines 15 - 17, Add an explicit
job-level permissions block to the validate-inputs job to drop the runner token
scope; update the validate-inputs job (named "validate-inputs") to include a
permissions: none entry so the job runs without any GitHub token privileges
since it only validates a string and exits.
| validate-inputs: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Validate version format | ||
| env: | ||
| # Pass through env so the shell sees a plain string, not an interpolated expression. | ||
| INPUT_VERSION: ${{ inputs.version }} | ||
| run: | | ||
| # Allow bare semver (1.2.3) or with pre-release/build suffix (1.2.3-rc.1). | ||
| # Reject anything else — especially characters that are shell metacharacters. | ||
| if [ -n "$INPUT_VERSION" ] && ! echo "$INPUT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+([.\-][a-zA-Z0-9._-]+)?$'; then | ||
| echo "::error::inputs.version must be semver (e.g. 1.16.4), got: ${INPUT_VERSION}" | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
validate-inputs inherits id-token: write from the workflow scope.
Workflow-level permissions: (lines 15–17) grants id-token: write, which is needed by the publish jobs for OIDC provenance, but validate-inputs only echoes and exits. Drop its scope explicitly so a future bug in this job can't mint an OIDC token.
🛡️ Proposed change
validate-inputs:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
steps:
- name: Validate version format🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/npm-publish.yaml around lines 23 - 36, The validate-inputs
job currently inherits workflow-level id-token: write; modify the
validate-inputs job (the job named validate-inputs) to explicitly restrict
permissions by adding a permissions block that sets id-token: none so it cannot
mint OIDC tokens (keep other workflow permissions unchanged); update the job
definition to include that permissions stanza directly under the validate-inputs
job header to override the workflow-level permission.
yottahmd
left a comment
There was a problem hiding this comment.
Thank you very much for hardening github workflows!
|
Thanks, great! ICYMI, I think you now can change repo settings to restrict your default workflow permissions. |
Claude Code with Opus 4.7. Not a blocker, just flagging. Happy to split.
Summary by CodeRabbit