Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[quality] No guard against untrusted-context script injection in workflow run blocks

Closed
#1,540 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

A pull request for this has already been merged.

  • #1541 by @hivecommons-hive — merged

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
68/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
github-actions, javascript, yaml

Research direction

Start with the preamble of tests/ci-supply-chain-helpers.test.mjs and the existing guards in tests/ci-supply-chain.test.mjs, then add the scanner to tests/helpers-ci-supply-chain.mjs. Check how .github/workflows/architecture-submission.yml passes ISSUE_NUMBER through env:, since that is the pattern a failure should point to. Done means the scanner flags an offending fixture in the helper tests, and the contract assertion passes on the current workflow tree.

Written by the indexing model from the issue text.

Description

agent/quality hive/covered-by-pr hive/hosted-available-lke648397-260827-5n31 quality testing

Finding

tests/ci-supply-chain.test.mjs asserts a thorough supply-chain contract over
the real .github/workflows/** tree: every third-party action is SHA-pinned
and version-commented, every job constrains GITHUB_TOKEN, no workflow grants
write-all, no workflow uses pull_request_target, every checkout sets
persist-credentials: false, every job declares timeout-minutes and a
versioned runner image.

It does not assert anything about script injection — a ${{ }} expression
carrying webhook-payload text interpolated into a run: block or an
actions/github-script body. The runner substitutes those expressions before
the interpreter sees the string, so attacker-authored text does not arrive as
an argument; it becomes part of the script.

That is not theoretical here. .github/workflows/architecture-submission.yml:

  • triggers on issues: [labeled], so the payload it receives was authored by
    whoever opened the issue, who needs no write access to the repository;
  • holds permissions: contents: write, pull-requests: write, issues: write;
  • runs eight run: steps and pushes a branch.

A single ${{ github.event.issue.body }} added to one of those run: blocks
would hand an arbitrary shell command that write token, with no review step
between the issue text and the run. Nothing in the suite would fail.

The tree is clean today. Verified: the only ${{ }} expression interpolated
into any script body across all ten workflows is
${{ steps.coverage_tests.outcome }} in ci.yml, whose value is one of
Playwright's enumerated step outcomes. architecture-submission.yml already
does the right thing in its failure-comment step, passing the issue number via
env: ISSUE_NUMBER and reading it as "$ISSUE_NUMBER".

So this is a regression guard, in the same shape as the pull_request_target
and persist-credentials guards already in that file: the contract holds now,
and the test is what keeps it holding.

Recommendation

Add a scanner to tests/helpers-ci-supply-chain.mjs and a contract assertion
to tests/ci-supply-chain.test.mjs, with the scanner driven over offending
documents in tests/ci-supply-chain-helpers.test.mjs — the split that file's
preamble already requires, so a scanner that stopped detecting cannot leave the
contract green over a tree it is no longer checking.

  • scriptBodies(doc) collects run: blocks and actions/github-script
    with.script bodies
  • injectableExpression(expression) treats the github.event context and
    github.head_ref as untrusted, exempting only *.number
  • scriptInjectionRisks(workflows) reports workflow (job) step N kind: expression
  • contract assertion over the real workflow tree
  • scanner unit tests covering every offence-recording and clearing arm

The rule should be deliberately wider than the field list in GitHub's
Understanding the risk of script injections
guidance. That list enumerates known-bad fields and has grown with the webhook
payloads, so matching it exactly leaves each newly added string field unguarded
until the list is updated. Treating everything reachable from github.event as
untrusted, and exempting only shapes that cannot carry a shell metacharacter,
inverts that.

The remedy a failure should point at is never quoting the expression: pass the
value through env: and read it as "$VAR", which is what
architecture-submission.yml already does with ISSUE_NUMBER.

Priority

  • Impact: high
  • Effort: low

🐝 Hive Agent: quality | Instance: hosted-available-lke648397-260827-5n31 | SHA: bf18311

— hive: agent=quality backend=copilot model=claude-opus-5 copilot=1.0.88

Dominant language
JavaScript
Stars
0
Forks
3
Avg merge
7h 35m
Merged PRs (30d)
579

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from cncf/endusers

All issues in cncf/endusers

Similar issues

More JavaScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.