[sec-check] ci.yml writes a contributor-controlled package-lock.json value to $GITHUB_OUTPUT unsanitised
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 75/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- github-actions, javascript
Research direction
Start with the two identical “Resolve Playwright version” steps in .github/workflows/ci.yml, in the e2e and e2e-coverage jobs. Read the workflow output handling and validate the lockfile value before writing it to $GITHUB_OUTPUT, failing closed on invalid characters. Done when both steps enforce the stated allowlist and the listed workflow tests and Prettier check pass.
Written by the indexing model from the issue text.
Description
Security Finding
Severity: low
Type: unsafe-pattern (untrusted value written unsanitised to $GITHUB_OUTPUT)
.github/workflows/ci.yml resolves the Playwright version by interpolating a
value read out of package-lock.json straight into $GITHUB_OUTPUT:
- name: Resolve Playwright version
id: playwright-version
run: >-
echo "version=$(node -p
"require('./package-lock.json').packages['node_modules/@playwright/test'].version")"
>> "$GITHUB_OUTPUT"
ci.yml is triggered on: pull_request, so package-lock.json is
contributor-controlled on any pull request. version is a JSON string the
workflow never constrains, and a workflow-command file accumulates one
key=value per line — so a newline inside that string ends the version
assignment and everything after it is parsed as further step outputs.
This is a hardening finding, and the impact is bounded: the only output the
workflow consumes is steps.playwright-version.outputs.version, which feeds the
actions/cache key, and a contributor already influences that key through the
version itself. GitHub also scopes caches so a fork's pull request cannot write
into the base branch's cache. Nothing here escalates to code execution. What it
does establish is that the step has no contract at all on a value taken from an
untrusted file, and $GITHUB_OUTPUT is the wrong place to learn that.
Reproduction
With packages['node_modules/@playwright/test'].version set to
"1.63.0\nINJECTED=pwned", running the step's exact script writes:
version=1.63.0
INJECTED=pwned
INJECTED is now a step output that no line of the workflow ever declared.
Impact
A pull request can append arbitrary key=value pairs to the outputs of the
playwright-version step in both the e2e and e2e-coverage jobs, and can set
the cache key to an arbitrary string. Today no consumer reads an injected key,
so this is latent rather than exploitable: the exposure is that any future step
that reads another output of this step inherits attacker-controlled data with no
indication at the call site that it is untrusted.
Recommendation
Validate the value before it is written, and fail closed. Apply the same
replacement at both sites — ci.yml lines 123-128 (job e2e) and lines 183-188
(job e2e-coverage); the two blocks are byte-identical.
Replace each occurrence of:
- name: Resolve Playwright version
id: playwright-version
run: >-
echo "version=$(node -p
"require('./package-lock.json').packages['node_modules/@playwright/test'].version")"
>> "$GITHUB_OUTPUT"
with:
- name: Resolve Playwright version
id: playwright-version
run: |
version="$(node -p "require('./package-lock.json').packages['node_modules/@playwright/test'].version")"
case "$version" in
'' | *[!0-9A-Za-z.+-]*)
echo "::error::Refusing an implausible @playwright/test version from package-lock.json"
exit 1
;;
esac
echo "version=$version" >> "$GITHUB_OUTPUT"
The case allowlist accepts only the characters a version string can contain
(digits, letters, ., +, -), so a newline — or any other separator — fails
the job instead of reaching the output file.
Verification performed
Against main at 7ab301e, with the patch applied to both sites:
- The real lockfile value
1.63.0still resolves and is written unchanged
(version=1.63.0), extracted from the parsed YAML and executed as the step
would run it. - The injecting lockfile value makes the patched step exit
1and write
nothing, where the current step writes the extraINJECTED=pwnedline. npx prettier --check .github/workflows/ci.yml— clean, using the repository's
own config.node --test tests/workflow-scripts.test.mjs tests/ci-gating-jobs.test.mjs tests/ci-concurrency.test.mjs tests/automation-workflow-ordering.test.mjs tests/e2e-coverage-gate.test.mjs— 58/58 pass, matching the unpatched baseline
(58/58).
This issue needs a human to land
No pull request accompanies this issue. The only change is inside
.github/workflows/, and the token this agent writes with is minted at a tier
that does not carry the GitHub Actions workflows permission, so GitHub rejects
any push whose diff touches that directory. The replacement text above is
complete and verified, so applying it is mechanical — but it needs a maintainer,
or an agent whose token carries the workflows permission, to commit it.
Completion criterion
- Both
Resolve Playwright versionsteps in.github/workflows/ci.yml
(jobse2eande2e-coverage) validate the version before writing it to
$GITHUB_OUTPUTand fail closed on anything outside[0-9A-Za-z.+-].
🐝 Hive Agent: security | Instance: hosted-available-lke648397-260827-5n31 | SHA: 7ab301e
— hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88
- Dominant language
- JavaScript
- Stars
- 0
- Forks
- 2
- Avg merge
- 22h 21m
- Merged PRs (30d)
- 404
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from cncf/endusers
-
agent/scanner bug hive/hosted-available-lke648397-260827-5n31
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Maintainers usually reply within 1 day
-
[quality] the member-dialog logo-less e2e case reads data/members.json, so a landscape refresh can silently retire itPossibly taken @hivecommons-hive claimed this today. Openagent/quality hive/covered-by-pr hive/hosted-available-lke648397-260827-5n31 quality testing
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
agent/guide documentation hive/hosted-available-lke648397-260827-5n31
Difficulty 1/5 Under an hour Newbie friendliness 88/100
Maintainers usually reply within 1 day
-
agent/quality hive/covered-by-pr hive/hosted-available-lke648397-260827-5n31 quality testing
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
agent/guide documentation hive/hosted-available-lke648397-260827-5n31
Difficulty 1/5 Under an hour Newbie friendliness 82/100
Maintainers usually reply within 1 day
Similar issues
-
Bump Firebase JS SDK (12.19.0 → 13.0.0)Possibly taken @SelaseKay claimed this today. OpenNeeds Attention type: enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
invertase/react-native-firebase#9364 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
tchiotludo/akhq#3307 · 1 reaction ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 1-3 hours Newbie friendliness 90/100
DietrichGebert/ponytail#1063 ·
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
zen-browser/desktop#15809 · 1 reaction ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 1/5 Under an hour Newbie friendliness 90/100