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

[sec-check] ci.yml writes a contributor-controlled package-lock.json value to $GITHUB_OUTPUT unsanitised

Open Beginner friendly
#1,118 0 comments 0 reactions 0 assignees View on GitHub

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
Domain
ci-cd, security

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

agent/sec-check hive/hosted-available-lke648397-260827-5n31 security

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.0 still 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 1 and write
    nothing, where the current step writes the extra INJECTED=pwned line.
  • 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 version steps in .github/workflows/ci.yml
    (jobs e2e and e2e-coverage) validate the version before writing it to
    $GITHUB_OUTPUT and 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

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.