ECLI-012: Privileged workflows execute branch-controlled npm lifecycle code

Open
#518 0 comments 0 reactions 1 assignee View on GitHub

@margaretjgu is already working on this.

Since Aug 6, 2026.

Assessment

This issue has not been assessed yet.

Description

security

Severity: Medium

Problem

.github/workflows/regenerate-notice.yml:21–57 and .github/workflows/schema-auto-fix.yml:17–71 both use write-capable tokens. The former grants contents: write; the latter grants contents: write and pull-requests: write. Both check out branch or PR content and run npm ci, which can execute the root prepare script and dependency lifecycle hooks. schema-auto-fix.yml also invokes the branch-controlled build and build:schema scripts. Modified branch code therefore runs while a write token is available.

Both workflows have access controls:

  • regenerate-notice.yml blocks fork PRs via github.event.pull_request.head.repo.full_name == github.repository.
  • schema-auto-fix.yml requires MEMBER/OWNER/COLLABORATOR author association and rejects cross-repository PRs.

The exposure is limited to authorized collaborators and compromised internal branches, not arbitrary external contributors. That lowers the severity relative to an unguarded pull_request_target, but a compromised collaborator account or bad dependency merge can still execute code with write privileges.

Fix

Use different remediations for the two workflows. The required security property is that PR-controlled code never executes in a job with write permissions or secrets.

regenerate-notice.yml: remove PR auto-commit

The repository already runs npm run test:notice in read-only CI. Remove the write-capable pull_request path from regenerate-notice.yml and require contributors or dependency-update automation to run npm run generate:notice and commit NOTICE.txt when that check fails.

If automatic PR updates are required, use two separate workflows:

  1. A pull_request workflow with contents: read, no secrets, and persist-credentials: false generates and uploads NOTICE.txt.
  2. A privileged workflow_run workflow, whose definition comes from the default branch, downloads the artifact from the exact triggering run and commits it.

Do not put the privileged writer in the same PR-controlled workflow. The writer must verify that the triggering workflow succeeded, the PR is same-repository, and the PR head still equals the SHA used to generate the artifact.

schema-auto-fix.yml: split generation and commit jobs

Because issue_comment uses the workflow definition from the default branch, this workflow can safely use two jobs on separate fresh runners:

  1. generate-schema:

    • permissions: contents: read, pull-requests: read
    • No secrets.
    • Resolve and record the exact PR headRefOid; reject cross-repository PRs.
    • Check out that SHA with persist-credentials: false.
    • Upload only docs/cli/schema.json using a SHA-pinned upload-artifact.
  2. commit-schema:

    • needs: generate-schema
    • permissions: contents: write, pull-requests: write
    • Run on a fresh runner and do not invoke npm, node, or any PR-controlled script.
    • Query the PR again and fail if its current head differs from the generation SHA.
    • Check out the exact SHA using the write-capable token. Credential persistence is acceptable only in this isolated writer job because it executes no PR-controlled code; alternatively, perform the commit through an explicitly authenticated GitHub API operation.
    • Require a regular JSON file, enforce a size limit, and validate it with jq -e.
    • Copy it to the fixed docs/cli/schema.json destination.
    • Assert that this is the only changed path.
    • Commit and push using --force-with-lease against the validated SHA.

Add npm ci --ignore-scripts to unprivileged generation jobs where compatible. This is defense in depth: it does not stop explicitly invoked PR-controlled scripts such as build:schema. The primary control is running those scripts without write permissions or secrets.

Risk

Medium. Current guards keep this outside the external-contributor boundary. The remaining exposure is to trusted collaborators and compromised internal branches.


Copied from the security review in elastic/infosec#27626 (ECLI-012).

Dominant language
TypeScript
Stars
43
Forks
24
Avg merge
1d 6h
Merged PRs (30d)
59

Contributor guide

Open the contributing guide

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 elastic/cli

All issues in elastic/cli

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.