Skip to content

Give CI's token read-only access, and lock source-map-js at 1.2.2 - #72

Merged
ww-mw merged 3 commits into
mainfrom
ci-read-only-token-and-source-map-js
Oct 11, 2026
Merged

ww-mw merged 3 commits into
mainfrom
ci-read-only-token-and-source-map-js

Conversation

@ww-mw

@ww-mw ww-mw commented Oct 11, 2026

Copy link
Copy Markdown
Member

Two hygiene alerts. 2f3ed86 answers the first and e293261 the second. c5a9370 then corrects two statements in the comment that 2f3ed86 added. None of the three touches src/, so dist/ is unchanged: npm run build on the branch leaves it byte for byte as it was.

1. CodeQL actions/missing-workflow-permissions: ci.yml (code-scanning alert 1)

Exposure. ci.yml declared no permissions, so its GITHUB_TOKEN got the repository default, and that default is write (default_workflow_permissions: "write"). The last CI run on main (run 38112853444, at b49217a) logs 17 scopes at write, among them Contents, PullRequests, Statuses, Actions, Issues, Packages and Pages. Only Metadata, Models and VulnerabilityAlerts are at read. On a push to main, or a pull request from a branch in this repository (only fork pull requests get a read-only token), any step could have written to the repository with that token, including a compromised action or a dependency's install script under npm ci. Checkout keeps the token available to later steps (persist-credentials: true). It writes the token to a credentials file in the runner's temp directory, and includeIf entries in .git/config point at that file. A later step can read the file directly or let git use it.

Change. A top-level permissions: contents: read, with a comment saying why (2f3ed86). The block's shape and placement follow mathworks/data-explorer-vscode@03f61fd. The comment is reworded: vscode's says checking out is the only thing the token is for, which leaves out setup-node; this one accounts for setup-node and its cache too. c5a9370 corrects two statements in that comment:

  • setup-node reads with the token only when the runner's tool cache lacks the Node version asked for;
  • the repository default grants write to almost every scope, not to all of them.

The job keeps its name, build-and-test, which is the required status check of the Protect main ruleset. release.yml is untouched: it already declares contents: write for its Release, which is why only ci.yml was flagged.

Why contents: read is enough:

  • actions/checkout only reads the repository.
  • actions/setup-node uses the token for one thing: looking up the Node release manifest on actions/node-versions. That is a read, and it happens only on a tool-cache miss. In run 38112853444, setup-node logged Found in cache @ /opt/hostedtoolcache/node/22.23.3/x64 and looked nothing up.
  • setup-node saves the npm cache with the runner's ACTIONS_RUNTIME_TOKEN, which permissions does not govern. data-explorer-vscode has run with exactly this block since 03f61fd, on the same checkout@v7 and setup-node@v7. Its last push to main (run 38113832364) logs the token as Contents: read and Metadata: read. The same run then logs Cache mode: write, Acquiring 20.20.2 - x64 from https://github.com/actions/node-versions/…, and Cache saved with the key: …. So both a tool-cache miss and a cache save have already run under this block.
  • Nothing in npm ci or npm run verify reads a token. git grep on the tracked tree finds none of GITHUB_TOKEN, GH_TOKEN, github.token, api.github.com, ACTIONS_RUNTIME or octokit.

Checked:

  • The file parses to the same name, on and jobs as before, plus permissions: {contents: read} (js-yaml at 2f3ed86, Ruby YAML at c5a9370). No line is longer than 78 columns.
  • CodeQL 2.27.2, the version of GitHub's latest scan of main (at b49217a, which still reports the alert; 2.27.1 first raised it on 2026-10-01), was run locally with its actions-code-scanning suite (17 rules) over the two workflow files. At b49217a it reports actions/missing-workflow-permissions at ci.yml 11:5-19:29, exactly where GitHub's alert is. At this branch's head it reports nothing. The alert should therefore close on the first scan of main after the merge.

2. Dependabot: source-map-js 1.2.1 → 1.2.2 (alert 1, GHSA-68fv-2mgg-jv7q / CVE-2026-93749, high)

Exposure. source-map-js through 1.2.1 accepts any offset.line in an indexed source map's sections. A tiny map with a huge offset blocks the event loop while its mappings are copied out. Here it is dev-only and transitive. It comes in through vitest (vite → postcss) and through @vitest/coverage-v8 (magicast), and both of those require ^1.2.1. 1.2.2 refuses a section offset that is not a non-negative integer, and a line offset past 1e7 (nested sections included).

What reaches a consumer. A consumer's own node_modules gets only dist/ (check:pack). A git-dependency install does fetch source-map-js, though, and that is how data-explorer-vscode takes core (github:mathworks/data-explorer-core#v1.36.3).

  • Why: core declares prepare (and build; npm prepares a git dependency that declares any of build, prepare, prepack, preinstall, install or postinstall). So npm clones core into a temporary directory and runs npm install --force --include=dev … there, using the clone's own lockfile, before it packs the clone.
  • Why it does no harm: prepare is only tsc, and typescript has no dependencies. Nothing in that clone parses a source map.
  • Reproduced with npm pack git+file://<this repo>#<ref> and an empty npm cache. With v1.36.3 as the ref, the clone's install fetched source-map-js-1.2.1.tgz. With this branch as the ref, it fetched source-map-js-1.2.2.tgz. In both cases the packed tarball holds only dist/, package.json, README.md and LICENSE. (This machine's npm sets ignore-scripts=true, so prepare itself was skipped. The dev install in the clone runs either way.)
  • v1.36.3 is b49217a, so a consumer pinned there keeps fetching 1.2.1 into that clone until its pin moves past this change.

e293261's message says source-map-js "never reaches a consumer", because the packed tarball is dist/ only. That sentence overstates the case. The paragraph above is the accurate account. Likewise, c5a9370's message calls CodeQL 2.27.2 "the version that raised the alert"; 2.27.1 raised it, and 2.27.2 is the version of the latest scan of main.

Change. Three lines of package-lock.json: the node_modules/source-map-js entry's version, resolved and integrity.

Why the edit is hand-made. registry.npmjs.org cannot be reached from the machine this was prepared on (the proxy answers 502). npm there resolves through an internal mirror, and any resolved URL npm writes points at that mirror, which check:leak forbids in a committed lockfile. So the entry was edited by hand, and resolved stays on https://registry.npmjs.org/. The integrity hash is computed from the tarball's bytes, so it holds whichever registry serves them.

How it was proven:

  • Integrity, three ways.
    • The sha512 of the 1.2.2 tarball's bytes, computed locally, is sha512-KGj/8Y43…NXNP3Vw==. It equals the registry's dist.integrity.
    • The registry signs source-map-js@1.2.2:<integrity> with ECDSA. That signature verifies against npm's public key as published in sigstore/root-signing (targets/registry.npmjs.org/keys.json), fetched from GitHub rather than from the mirror. Changing one character of the integrity makes the verification fail.
    • As a control, the same hash of 1.2.1's tarball, from the same mirror, equals the integrity the lockfile already held.
  • Nothing else in the entry moves. The 1.2.2 tarball's package.json has no dependencies and no install script. It keeps license: BSD-3-Clause and engines.node >=0.10.0.
  • It is what npm would write. Run npm update source-map-js --package-lock-only on the old lockfile, then put its one changed resolved URL (the mirror's) back on registry.npmjs.org. The result is this file, byte for byte.
  • It is a fixed point. npm install --package-lock-only leaves the edited file byte for byte unchanged.
  • It installs. A real npm ci from it, with the default registry and the lockfile left untouched, installs 1.2.2. node -p "require('source-map-js/package.json').version" prints 1.2.2, and npm ls shows 1.2.2 for both dependents. Under Node 22.23.3 and npm 10.9.9, the versions CI used, npm ci added 77 packages and reported found 0 vulnerabilities. CI's npm ci at b49217a reported 1 high severity vulnerability.
  • The alert clears. npm audit --package-lock-only reports the high (GHSA-68fv-2mgg-jv7q) on the old lockfile and found 0 vulnerabilities on this one.
  • Leak check. All 101 resolved URLs are on https://registry.npmjs.org/, and npm run check:leak passes.

Verification

npm run verify passes at the branch head, c5a9370, in two local runs:

  • Node 22.23.3 with npm 10.9.9, the versions CI logged at b49217a, right after a fresh npm ci;
  • Node 24.14.1 with npm 11.11.0.

Both runs give the same results:

  • typecheck and build pass;
  • smoke reports 16 exports;
  • vitest: 205 test files passed and 1 skipped; 6,604 tests passed and 26 skipped. These are the counts CI logged at b49217a;
  • check:pack: 559 files, 684.5 kB;
  • check:leak passes, and so does check:browser (153 modules).

git status is clean after npm ci and after each verify run.

One local run differs from CI. This machine's npm config sets ignore-scripts=true, so the local npm ci ran no lifecycle scripts. With this lockfile, that skips two things:

  • core's own prepare, which is the same tsc build that verify runs next;
  • the install script of fsevents, which is optional and macOS-only, so CI never installs it.

ww-mw added 3 commits October 11, 2026 17:51
CodeQL actions/missing-workflow-permissions (code-scanning alert 1, on
ci.yml line 11): ci.yml declared no `permissions`, so its GITHUB_TOKEN got
the repository default, which is "write". The last CI run on main (run
38112853444, at b49217a) logs 17 scopes at write, among them Contents,
PullRequests, Statuses, Actions, Issues, Packages and Pages, and only
Metadata, Models and VulnerabilityAlerts at read.

The job needs none of that. Checkout reads the repository, and setup-node
uses the token only to look up the Node release manifest. Nothing in
`npm ci` or `npm run verify` reads a token: `git grep` finds no
GITHUB_TOKEN, GH_TOKEN, github.token or api.github.com in the tracked tree.

The npm cache is unaffected. setup-node saves it with the runner's
ACTIONS_RUNTIME_TOKEN, which `permissions` does not govern.
data-explorer-vscode has run this shape since its 03f61fd, on the same
checkout@v7 and setup-node@v7. Its last push to main (run 38113832364)
logs the token as Contents: read and Metadata: read, then "Cache mode:
write", a Node download from actions/node-versions, and "Cache saved with
the key".

release.yml already declares `contents: write` for its Release, which is
why only ci.yml was flagged; this matches that shape. The job keeps its
name, build-and-test, the ruleset's required check. The file parses with
js-yaml to the same name, on and jobs as before, plus permissions
{contents: read}.
Dependabot alert 1, GHSA-68fv-2mgg-jv7q (CVE-2026-93749, high):
source-map-js through 1.2.1 takes an indexed source map's per-section
offset.line as given, so a tiny map with a huge offset blocks the event
loop while its mappings are copied out. 1.2.2 refuses an offset that is not
a non-negative integer, and a line offset past 1e7, nested sections
included.

Here it is dev-only and transitive: vitest's vite -> postcss and
@vitest/coverage-v8's magicast, each asking ^1.2.1. It never reaches a
consumer, since the packed tarball is dist/ only (check:pack).

The entry's version, resolved and integrity are edited by hand, because
registry.npmjs.org is unreachable from where this was made and npm there
resolves through an internal mirror. The three values are what npm writes:

- The integrity is the sha512 of the 1.2.2 tarball's bytes, computed
  locally, and equals the registry's dist.integrity. The registry's
  signature over "source-map-js@1.2.2:<integrity>" verifies against npm's
  public key as sigstore/root-signing publishes it, and fails with one
  character of the integrity changed. The same hash of 1.2.1's tarball,
  from the same mirror, is the integrity this lockfile already held.
- 1.2.2 has no dependencies and no install script, and keeps license
  BSD-3-Clause and engines node >=0.10.0, so nothing else in the entry
  moves.
- `npm update source-map-js --package-lock-only` on the old lockfile
  writes this file byte for byte, once its one changed resolved URL, the
  mirror's, is put back on registry.npmjs.org.
- `npm install --package-lock-only` leaves this file byte for byte as it
  is.

`npm ci` from it installs 1.2.2 for both dependents (npm ls). `npm audit`
reports the high on the old lockfile and nothing on this one. All 101
resolved URLs are on registry.npmjs.org, and check:leak passes. `npm run
verify` passes under Node 24.14.1 with 205 test files and 6,604 tests
passed and 26 skipped, the counts CI logged at b49217a on Node 22. `npm
run build` leaves dist/ unchanged.
…e miss

The comment above `permissions` said the token is for checkout and for
setup-node's lookup of the Node release manifest, and so does 2f3ed86's
message. setup-node looks the manifest up only when the runner's tool
cache lacks the Node asked for. In run 38112853444 (push to main, at
b49217a) it logged "Found in cache @ /opt/hostedtoolcache/node/22.23.3/x64"
and fetched nothing. data-explorer-vscode's run 38113832364 missed the
cache and logged "Acquiring 20.20.2" from actions/node-versions. So the
lookup is the most setup-node does with the token, not what it does on
every run.

The comment also said the repository default can grant write to
everything. Under that default, run 38112853444 logs 17 scopes at write
and 3 at read (Metadata, Models and VulnerabilityAlerts), so "almost every
scope" is what the setting grants.

Only the comment changes. The file parses (Ruby YAML) to the same value
as before, and no line is longer than 78 columns. CodeQL 2.27.2, the
version that raised the alert, run with its actions-code-scanning suite:
on b49217a's workflows it reports actions/missing-workflow-permissions at
ci.yml 11:5-19:29, where GitHub's alert is, and on this commit's it
reports nothing.
@ww-mw
ww-mw merged commit 4999106 into main Oct 11, 2026
4 checks passed
@ww-mw
ww-mw deleted the ci-read-only-token-and-source-map-js branch October 11, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant