Skip to content

sec: harden workflow files against shell injection and over-privileged tokens#2046

Merged
yottahmd merged 3 commits intodagucloud:mainfrom
krlmlr:claude/review-workflow-safety-ha9UY
Apr 28, 2026
Merged

sec: harden workflow files against shell injection and over-privileged tokens#2046
yottahmd merged 3 commits intodagucloud:mainfrom
krlmlr:claude/review-workflow-safety-ha9UY

Conversation

@krlmlr
Copy link
Copy Markdown
Contributor

@krlmlr krlmlr commented Apr 27, 2026

  • 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).

Claude Code with Opus 4.7. Not a blocker, just flagging. Happy to split.

Summary by CodeRabbit

  • Chores
    • Enhanced security of CI/CD infrastructure through explicit permission restrictions across GitHub Actions workflows, limiting automated operations to minimum necessary access.
    • Implemented input validation gates for release and deployment workflows to prevent invalid parameters from proceeding.
    • Improved workflow reliability with standardized environment variable handling.

claude and others added 2 commits April 27, 2026 04:58
…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
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 58482a96-3747-4411-a87e-0ecc2ed824f8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Updates 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

Cohort / File(s) Summary
Permission Configuration
.github/workflows/chart-ci.yaml, .github/workflows/frontend-ci.yaml, .github/workflows/release.yaml
Adds workflow-level permissions declarations restricting token capabilities: contents: read for chart-ci and frontend-ci (read-only), and contents: write for release (enable GoReleaser write operations).
Input Validation Gates
.github/workflows/docker-manual.yaml, .github/workflows/manual-release.yaml
Introduces new validate-inputs jobs that gate downstream execution: docker-manual validates ref input for shell metacharacters; manual-release validates tag input against semver regex (vX.Y.Z format). Dependent jobs use needs: validate-inputs to enforce validation before proceeding.
Shell Script Refactoring with Validation
.github/workflows/npm-publish-test.yaml, .github/workflows/npm-publish.yaml
Adds validate-inputs jobs and refactors run: scripts to avoid direct ${{ ... }} interpolation by moving inputs and matrix values into env variables (INPUT_VERSION, MATRIX_*, PLATFORM), then referencing them as shell variables. Updates version-extraction logic and asset references to use env-based values, improving injection safety.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main changes: hardening workflow files against shell injection vulnerabilities and restricting token permissions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Missed interpolation: cd npm/${{ matrix.package }} still inside run:.

The PR description states all ${{ matrix.* }} interpolations inside run: blocks were replaced. This site (and ${{ matrix.package }} quoting in continue-on-error: ${{ matrix.arch == ... }} is fine since that's not in run:) was missed. matrix.package is 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-inputs can be bypassed for publish-main-package due to if: always().

Chain: validate-inputs → publish-platform-packages → publish-main-package. With needs: publish-platform-packages + if: always(), when validate-inputs fails, publish-platform-packages is skipped, but publish-main-package still runs because always() evaluates true regardless of upstream skipped status.

publish-main-package then proceeds with the unvalidated workflow-level VERSION env (computed from raw inputs.version) and exposes NODE_AUTH_TOKEN (line 250) to a step that runs npm version "$PKG_VERSION" and npm 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-latest

Apply 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 under workflow_dispatch.

inputs.version is required: 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 a release: trigger that has since been removed. Either re-add the trigger or drop the dead RELEASE_TAG_NAME plumbing 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 — case matching against $INPUT_REF is the right pattern.

The same env→shell-var rewrite repeated identically in docker-default, docker-dev, and docker-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 because INPUT_REF is passed via env: and double-quoted in case "$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

📥 Commits

Reviewing files that changed from the base of the PR and between 96dc410 and 72b2e24.

📒 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

Comment on lines +15 to +17
validate-inputs:
runs-on: ubuntu-latest
steps:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +23 to +36
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Copy link
Copy Markdown
Collaborator

@yottahmd yottahmd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for hardening github workflows!

@yottahmd yottahmd merged commit 56eda33 into dagucloud:main Apr 28, 2026
1 check passed
@krlmlr
Copy link
Copy Markdown
Contributor Author

krlmlr commented Apr 28, 2026

Thanks, great! ICYMI, I think you now can change repo settings to restrict your default workflow permissions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants