Skip to content

fix(deps): bump js-yaml from 4.1.1 to 4.3.2 (CVE-2026-84375) - #42

Merged
SeriousJul merged 1 commit into
mainfrom
factory/48-cve-2026-84375-js-yaml-maxtotalmergekeys-does-not-limit-cpu-use-for-empty-merge-sources
Sep 25, 2026
Merged

SeriousJul merged 1 commit into
mainfrom
factory/48-cve-2026-84375-js-yaml-maxtotalmergekeys-does-not-limit-cpu-use-for-empty-merge-sources

Conversation

@SeriousJul

Copy link
Copy Markdown
Owner

Summary

Re-pin the transitive dependency js-yaml from 4.1.1 to 4.3.2, the first patched version for CVE-2026-84375. The change is lockfile-only: package.json is untouched, and the diff is one node_modules/js-yaml block.

Field Value
Package js-yaml
Ecosystem npm
Manifest package-lock.json
Relationship transitive, one flat copy
Installed before 4.1.1
This change 4.3.2 (npm v4-legacy tag)
Vulnerable range >= 4.0.0, < 4.3.2
Severity high, CVSS 7.5
Advisory GHSA-2883-xcg3-v3hh
Upstream fix nodeca/js-yaml#797
Source item https://github.com/SeriousJul/seriousjul.github.io/security/dependabot/48

The source item stays open. Closing a finding is a maintainer decision made after this merges.

The defect

maxTotalMergeKeys charges one budget unit per folded key. A merge source that folds no keys, an empty {} mapping, costs real work to process but adds nothing to the counter. Alias one sequence of N empty mappings into K merge targets and the loader does O(N * K) work while totalMergeKeys never moves, so no configured limit can stop it. 4.3.2 charges one unit per merge source on top of one per key.

Why 4.3.2, and why nothing smaller or larger

  • Smallest change out of the range. All five consumers (@docusaurus/utils, @docusaurus/utils-validation, @docusaurus/plugin-content-docs, cosmiconfig, @11ty/gray-matter) require ^4.1.0, which 4.3.2 satisfies. No manifest edit, no overrides block, no range rewrite.
  • 4.3.1 is not enough. It is the first patched version for the three older js-yaml alerts below, and it sits inside this advisory's range: it counts keys only, so an empty source is still free.
  • 5.x is not the target. The npm latest is 5.4.2, but .github/dependabot.yml ignores semver-major bumps for *, 5.x is a different line with changed merge defaults, and 4.3.2 is the release upstream ships these fixes on.

This also clears the three other open js-yaml alerts

4.1.1 sits inside the vulnerable range of every one of them, so one pin resolves all four:

Alert Advisory Range First patched Severity
#48 CVE-2026-84375 / GHSA-2883-xcg3-v3hh >= 4.0.0, < 4.3.2 4.3.2 high
#29 GHSA-5p4m-2wfm-xmqj, quadratic !!omap >= 4.0.0, < 4.3.1 4.3.1 high
#16 CVE-2026-59869 / GHSA-52cp-r559-cp3m >= 4.0.0, < 4.3.0 4.3.0 high
#12 CVE-2026-53550 / GHSA-h67p-54hq-rp68 >= 4.0.0, <= 4.1.1 4.2.0 medium

This PR targets only #48, the assigned item. The other three close as a side effect.

What 4.1.1 actually did, measured

Node 26.8.2, npm 11.19.1, one machine. The advisory PoC document, at three sizes:

N (sources = targets) Document js-yaml 4.1.1 js-yaml 4.3.2
800 ~13 KB parsed, 7.4 ms refused, 2.1 ms
3200 ~50 KB parsed, 52.3 ms refused, 2.6 ms
20000 ~313 KB parsed, 1839.6 ms refused, 5.9 ms

Superlinear, as published. Two notes that did not come from the advisory text:

  1. On 4.1.1 there is no guard to bypass, because there is no guard at all. maxTotalMergeKeys arrived in 4.2.0 and became an option on the 4.x line in 4.3.0. 4.1.1 does not read it, so load(src, { maxTotalMergeKeys: 1000 }) was accepted and ignored: measured at 1756.1 ms, no throw. This repo cannot mitigate the flaw by configuration; the dependency has to move.

  2. On 4.3.2 the PoC is stopped by a different check than the one this CVE is about. 4.3.2 also added a hard cap of 100 items per merge sequence, so a 20000-element alias dies with abnormal merge sequence size, not with the budget message. Confirmed by probe, not just by release note: a 101-element merge sequence parses on 4.3.0 and 4.3.1 and is refused on 4.3.2. Upstream lists both halves of the release:

    ## 4.3.2 - 2026-08-26
    ### Changed
    - [backport] Hard-limit merge sequence size to 100.
    
    ### Security
    - [backport] Count empty mappings in merge sequences toward `maxTotalMergeKeys`
      to limit CPU usage, #797.
    

    The cap is why the tests below do not lean on the published PoC as their proof.

The proof of the fix, isolated from the cap

The cost model is readable directly, with budgets small enough to see one unit at a time and merge sequences far below any length cap. Measured by swapping each version into the project node_modules:

Probe cost on 4.3.0 / 4.3.1 cost on 4.3.2 4.3.0 / 4.3.1 4.3.2
1 empty source, budget 0 0 1 parses throws
2 empty sources, budget 1 0 2 parses throws
1 source with 4 keys, budget 4 4 5 parses throws
100 empty sources x 101 targets 0 10100 parses throws
100 empty sources x 100 targets 0 10000 parses parses

One unit per source plus one unit per folded key is the model. The last row is the negative control: the guard stops the hostile count at the documented default of 10000 and still permits exactly 10000 units, so it is not a blanket ban on merges.

Reachability: this is CI hygiene, not a live exposure on the site

Traced rather than assumed.

Fact Evidence
js-yaml never reaches the browser grep -ro "js-yaml" build returns 0 hits across the 38 generated pages
It runs at build time, on repo-authored input MDX front matter goes through @11ty/gray-matter/lib/engines.js:16, parse: yaml.load.bind(yaml)
Other call sites are config files in this repo cosmiconfig/dist/loaders.js:50 on yaml.load(content), @docusaurus/plugin-content-docs sidebars, @docusaurus/utils/lib/dataFileUtils.js, @docusaurus/utils-validation tags file
Both real entry points pass no options so the default 10000 budget is the only thing in force, which is why the test asserts the no-option case
One path does take untrusted input .github/workflows/ci-cd.yml runs build and test on pull_request, so a fork PR can add an MDX file whose front matter reaches yaml.load in GitHub Actions

The CVSS 7.5 assumes a service parsing attacker-supplied YAML, which this site is not. What this repo does have is a fork-PR route that burns CI CPU, unbounded on 4.1.1. The value of the pin is that the route closes and npm audit stops reporting js-yaml.

Coordination with Dependabot PR #41

Dependabot's open group PR #41, chore(deps): bump the npm_and_yarn group ... with 20 updates, also moves js-yaml 4.1.1 to 4.3.2. This PR does not compete with it. Both sides write the identical block, so the three-way state matches line for line:

$ git merge-tree --write-tree pr-41 <this-branch>   -> clean, exit 0
$ git merge-tree --write-tree <this-branch> pr-41   -> clean, exit 0

The merged tree keeps js-yaml@4.3.2 in both merge orders, with the same version, resolved and integrity. Whichever lands first, the other stays clean, and #41 remains needed for the other 19 packages.

Note that #41 is a group requirements update, so GitHub does not list it as a fix PR for alert #48: the alert's dependency_track is null. Merging #41 does drop the resolved version out of the range, and the alert clears on the next scan either way. This PR exists to give the advisory its own reviewed, tested commit, and to unblock #48 from #41's 19 unrelated bumps.

API compatibility

4.1.1 and 4.3.2 export the same 14 names (load, loadAll, dump, safeLoad, safeDump, Schema, Type, the four schemas, types, YAMLException). Nothing the four consumers call, load and dump, moved, so no migration was needed. Confirmed by npm run build, not only by inspection.

Verification

Check Result
npm ci from the committed lockfile resolves js-yaml@4.3.2, one flat copy
npm run typecheck pass
npm run build [SUCCESS] Generated static files in "build", 38 HTML pages
npm test 48 of 48 pass, 20 new js-yaml tests plus 28 existing shell-quote tests
npm audit js-yaml entries none. 40 other advisories stay open, each on its own alert
Sensitivity of the new test file 4.1.1 fails 10 of 20; 4.2.0 fails 9 of 20; 4.3.0 fails 10 of 20; 4.3.1 fails 10 of 20; 4.3.2 passes 20 of 20

The new file is tests/js-yaml-security.test.mjs. Beyond the version floor it covers four things: the exact cost model above, that the bound holds with no caller-supplied options and through the real @11ty/gray-matter engine, that normal merge use keeps its documented precedence, and that every front matter block and every YAML file tracked in this repo still parses.

Deliberately not done

  • No overrides or resolutions entry. Every dependent range already admits 4.3.2, so a lockfile pin is sufficient, and an override would hide future upstream fixes.
  • No move to the 5.x line. See above.
  • No repo-wide Prettier pass. npx prettier --check reports 25 pre-existing files outside this change. The new file is Prettier-clean, and reformatting 25 unrelated files here would make a security diff unreviewable and conflict with chore(deps): bump the npm_and_yarn group across 1 directory with 20 updates #41.

Re-pin the transitive dependency js-yaml in package-lock.json from 4.1.1 to
4.3.2, the first patched version for GHSA-2883-xcg3-v3hh / CVE-2026-84375.
The maxTotalMergeKeys budget did not charge an empty merge source, so a
document that aliases one large sequence of {} mappings into many targets
did O(sources x targets) work without ever moving the counter.

All five consumers require ^4.1.0, so the bump is lockfile-only: no
package.json change, and no dependent range is left unsatisfied.

4.3.2 also clears the three other open js-yaml alerts on this repo, whose
ranges all include 4.1.1: #12 (CVE-2026-53550), #16 (CVE-2026-59869) and
#29 (GHSA-5p4m-2wfm-xmqj).

Add tests/js-yaml-security.test.mjs, which pins the corrected cost model
(one budget unit per merge source, on top of one per folded key) and keeps
normal merge use and this repo's own YAML parsing intact. Verified to fail
10 of 20 tests on 4.1.1, 4.3.0 and 4.3.1, and to pass 20 of 20 on 4.3.2.

Alert: https://github.com/SeriousJul/seriousjul.github.io/security/dependabot/48

@SeriousJul SeriousJul left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #42

I checked this PR by reproducing it. I ran every command and every measurement below on this branch. Nothing here is taken from the PR text on trust.

  • Score: 93 / 100
  • Specification Check: Pass. Every acceptance point for alert #48 is met, and I reproduced each one. Details in section 1.
  • Quality Check: Pass, with one latent defect in the new test file. The dependency change itself is correct, minimal, and reproducible from the registry. Details in section 2.
  • Required Changes: One, in section 3. It is a one-line fix to an over-broad matcher in the new test file. Everything in section 4 is optional.

Outcome: Request changes. Nothing here questions the pin, the version choice, or the reachability analysis. I am asking for section 3 so the guard test cannot report a false diagnosis later.


1. Specification Check: Pass

The assigned item is Dependabot alert #48 (CVE-2026-84375). Every acceptance point is met, and each one is independently confirmed.

The advisory is what the PR says it is. From the public advisory database, GHSA-2883-xcg3-v3hh / CVE-2026-84375, js-yaml, severity high, CVSS 3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (base score 7.5), range >= 4.0.0, < 4.3.2, first patched 4.3.2. The flaw text matches the PR description exactly: the budget does not charge an empty mapping, so N sources merged K times costs O(N * K) work with no change to the counter.

Acceptance point Result How I confirmed it
Resolved version sits outside >= 4.0.0, < 4.3.2 Pass node -e require('js-yaml/package.json').version after npm ci gives 4.3.2
Lockfile is the only production change Pass Diff is 13 added / 3 deleted lines in one node_modules/js-yaml block; package.json untouched
Integrity matches the registry, not a hand-edit Pass Committed sha512-SFNOvSJ+Dgf... equals npm view js-yaml@4.3.2 dist.integrity
The lockfile is what npm itself would write Pass npm install --package-lock-only regenerates the committed file byte for byte. This is the strongest check here, and it rules out a manual edit
No second copy left vulnerable Pass npm ls js-yaml --all shows five consumers, all deduped to one flat js-yaml@4.3.2. find node_modules -type d -name js-yaml returns one path
Bump satisfies every consumer range Pass All five declare ^4.1.0. 4.3.2 is the highest 4.x on the registry, so the upper bound of ^ also holds
npm audit no longer reports js-yaml Pass Zero js-yaml entries. 40 other packages stay flagged, as the PR states
Site still builds and type-checks Pass npm run typecheck clean. npm run build [SUCCESS], 38 HTML pages
Guard is live at the real call sites Pass @11ty/gray-matter/lib/engines.js does parse: yaml.load.bind(yaml), and cosmiconfig/dist/loaders.js:50 does yaml.load(content). Both pass no options, so the default budget is the only defence. The test targets exactly that
The pin cannot regress Pass ^4.1.0 excludes 5.x, and dependabot ignores semver-major, so no automatic update can roll it back. This is why the "no overrides" choice is right, not laziness
Coordination with Dependabot #41 Pass git merge-tree --write-tree pr41 <this> exits 0, and the merged tree holds 4.3.2 with the same resolved and integrity
New test really detects the flaw Pass See section 3
CI Pass build-and-test, CodeQL, and both Analyze jobs are green on the head commit

No missing requirement and no incorrect behaviour.

2. Quality Check: Pass

Design. The change is the smallest correct one. A lockfile pin, not an overrides block, is the right call: an override hides future upstream fixes and lies about the consumer contract. Keeping the fix isolated to one advisory commit, separate from the 20-package group in #41, is the right call for the same reason. Both rejections in "Deliberately not done" are correct and argued.

Test design is above average, and the reason matters. The budget probes use tiny budgets and sequences far below the 100-item cap. That isolates the CVE accounting from the unrelated guard that 4.3.2 also added. A naive test would assert "the PoC throws", which passes on 4.2.0 for the wrong reason. assertRejected reads the message instead of only the throw type, which closes that hole. The negative controls (exactly 10000 units parses, ordinary merge keeps its precedence) stop the suite from turning into a blanket ban. The repository corpus block gives npm test the same assurance the build job already gives, and names the failing file.

I confirmed the cost model myself, and the four numbers in the PR table are exact:

101-element merge sequence : parses on 4.3.0 and 4.3.1, throws `abnormal merge sequence size` on 4.3.2
2 empty sources at budget 1: parses on 4.3.0 and 4.3.1, throws `merge keys exceeded maxTotalMergeKeys (1)` on 4.3.2
100 x 100 with no options  : parses on 4.3.2 (exactly the default 10000)
100 x 101 with no options  : throws `merge keys exceeded maxTotalMergeKeys (10000)` on 4.3.2

Sensitivity is exactly as documented. I swapped each version into node_modules and re-ran the file: 4.1.1 fails 10 of 20, 4.2.0 fails 9, 4.3.0 fails 10, 4.3.1 fails 10, 4.3.2 passes 20 of 20. All five numbers match the header comment. The 4.3.1 row is the important one, and it is real.

Compatibility. 4.1.1 and 4.3.2 export the same 14 names, and the same single dependency argparse ^2.0.1. No engines field in either, so Node 20 is unaffected. I also checked the claim in the API test comment: 5.4.2 does drop safeLoad, safeDump, and safeLoadAll, so that test really would fail on a 5.x pin.

Runner and style. npm test gives 48 of 48 on Node 26.8.2 and on Node 20.20.2, which is the CI version. npx prettier --check tests/js-yaml-security.test.mjs is clean against the repo .prettierrc. Commit message follows conventional commits, and carries no agent co-author.

Security and performance. The whole file runs in about 60 ms on the pinned version. I also reproduced the vulnerable-version timing myself, on 4.1.1, with the same document shape: N = 800 parsed in 6.8 ms (13 KB), N = 3200 in 50.3 ms (53 KB), N = 20000 in 1744.3 ms (332 KB). So the guard is what keeps this file fast, and the file is a mild canary, not a slow suite. No new dependency, no network in tests, no secret handling.

3. Required changes

  • lockfileJsYamlEntries() matches a package that is not the dependency under test. The filter is /(^|\/)js-yaml$/, and that also matches node_modules/@types/js-yaml. I confirmed it: the regex returns true for node_modules/@types/js-yaml. js-yaml 4.x ships no type declarations, and @types/js-yaml exists on the registry, so that devDependency is a realistic addition to this repo later. When it lands, two things break at once: entries.length becomes 2 and the test fails with "a nested copy is a second resolved version that this pin does not cover", which is a false statement, and entry.version then compares the @types version against the installed runtime version. Anchor the pattern on the install path instead, for example /(^|\/)node_modules\/js-yaml$/. One line, and it keeps a guard test from producing a wrong diagnosis.

4. Optional, not blocking

  • DEFAULT_BUDGET = 10000 is asserted but never read from the library. The 100 x 100 and 100 x 101 pair encodes both the accounting model and the shipped default. If upstream ever tunes the default, the failure message is "the pinned js-yaml rejected a document this site needs to parse", which points at the wrong thing. The thrown message already carries the number: merge keys exceeded maxTotalMergeKeys (10000). Read it and assert it equals DEFAULT_BUDGET, then derive the two boundary documents from it.
  • satisfiesCaret() hard-fails on any range that is not literally ^X.Y.Z. A future ~4.3.2 or >= 4.1.0 < 5 would stop the suite with "not a plain caret range" rather than report whether the pin satisfies it. Loud is acceptable in a guard test. Handling ~ and >=, and reporting anything else as unjudged, would be better.
  • The entry.resolved assertion hard-codes https://registry.npmjs.org. This repo has no .npmrc, so it is fine today. Match the /js-yaml-<version>.tgz suffix, and the test survives a registry mirror.
  • The corpus block shells out to git ls-files. A source tarball or any non-git checkout makes the whole file error in execFileSync, with a message about git rather than about YAML. Wrap it and fall back to a directory walk.
  • semverParts() and isAtOrAbove() are now duplicated across two test files. isAtOrAbove is the same logic in both, and semverParts differs only by the package name in its message. A third security file should move both into tests/helpers/. I checked that this is safe: on Node 20.20.2 and 26.8.2, node --test tests/ runs only *.test.mjs, so a helper file is not collected as a test.
  • The test named refuses the advisory PoC shape loads with the default schema. The published PoC passes { schema: YAML11_SCHEMA }. Loading with no options is the better test for this repo, because merge is on by default in v4 and no call site here passes a schema. Say that in the name or the comment, so the difference from the advisory does not read as an omission.

5. Accuracy of the PR text

The code and the tests are right. Two figures in the description are wrong, and one differs from the published advisory for a reason worth stating. Fix them before merge so the record is clean.

  • npx prettier --check reports 40 tracked files outside this change, not 25. Under src/ alone it is 29. The decision (do not reformat 40 unrelated files inside a security diff) still holds. It is the right call.
  • The "same 14 names" list enumerates 13. safeLoadAll is missing from the prose. The count of 14 is correct.
  • The measured PoC row says ~313 KB and 1.8 s at N = 20000, where the advisory publishes ~500 KB and ~13 s. I ran it myself on 4.1.1 and got 332 KB and 1744 ms, which lands on the PR figure. The advisory size column is the loose one, and the published time is from different hardware. The PR table stands. Add one sentence saying the measurement is local, so a reader does not have to work that out.

6. Verdict

The dependency change is correct, minimal, reproducible from the registry, and complete for alert #48. It also clears #12, #16, and #29, whose ranges I verified all include 4.1.1. The test file is a real detection instrument, not decoration, and I reproduced its sensitivity on all five versions and its timing on 4.1.1. One latent over-broad matcher in that file is the only thing I am asking for before merge.

Fix section 3, correct the three figures in section 5, and this is good to merge.

@SeriousJul SeriousJul left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required change, section 3 of the review. This filter also matches node_modules/@types/js-yaml. I checked:

$ node -e "const re=/(^|\/)js-yaml\$/; for (const k of ['node_modules/js-yaml','node_modules/@types/js-yaml','node_modules/foo/node_modules/js-yaml']) console.log(k, re.test(k)))"
node_modules/js-yaml true
node_modules/@types/js-yaml true
node_modules/foo/node_modules/js-yaml true

js-yaml 4.x ships no type declarations (ls node_modules/js-yaml has no .d.ts and no types field), and @types/js-yaml is on the registry at 4.0.9, so that devDependency is a realistic later addition here. When it lands, entries.length becomes 2 and the assertion fails with "a nested copy is a second resolved version that this pin does not cover", which is a false statement about a sibling types package. It also then compares the @types version against the installed runtime version.

Anchor the pattern on the install path. Nested real copies still match, and the types package drops out:

const entries = Object.entries(lock.packages).filter(([key]) =>
  /(^|\/)node_modules\/js-yaml$/.test(key)
);

fs.readFileSync(path.join(repoRoot, 'package-lock.json'), 'utf8')
);
const entries = Object.entries(lock.packages).filter(([key]) =>
/(^|\/)js-yaml$/.test(key)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required change (section 3 of the review). This filter also matches node_modules/@types/js-yaml. Verified locally:

$ node -e "const re=/(^|\/)js-yaml$/; console.log(re.test('node_modules/@types/js-yaml'))"
true

js-yaml 4.x ships no type declarations, and @types/js-yaml is on the registry (4.0.9), so that devDependency is a realistic later addition to this TypeScript repo. When it lands, entries.length becomes 2 and the test fails with "a nested copy is a second resolved version that this pin does not cover", which is a false diagnosis about a sibling types package. entry.version then compares the @types version against the installed runtime version too.

Anchor on the install path so real nested copies still match and the types package drops out:

  /(^|\/)node_modules\/js-yaml$/.test(key)

@SeriousJul

Copy link
Copy Markdown
Owner Author

Review outcome note. The review above is a COMMENTED review event, not REQUEST_CHANGES, because GitHub rejects a changes-requested event on your own pull request. The verdict is still request changes: one blocking item, section 3 of the review, anchored inline at tests/js-yaml-security.test.mjs:203.

I applied the needs-work label to carry that state, and dropped ready-for-review.

Summary of the verdict:

  • Score 93 / 100
  • Specification Check: Pass. Every acceptance point reproduced independently.
  • Quality Check: Pass, with one latent defect in the new test file.
  • Required Changes: the over-broad /(^|\/)js-yaml$/ lockfile matcher. One line.
  • Optional items and three text corrections in sections 4 and 5 of the review.

@SeriousJul

Copy link
Copy Markdown
Owner Author

Clarification: I cannot delete a submitted review, so the short standalone comment above duplicates section 3. Its shell snippet has two typos (an extra ) and a stray backslash before $). The correct text is the review body and the inline comment on tests/js-yaml-security.test.mjs:203. Ignore the snippet in the standalone comment.

@SeriousJul
SeriousJul merged commit 8e5afc2 into main Sep 25, 2026
5 checks passed
SeriousJul added a commit that referenced this pull request Sep 25, 2026
Add tests/websocket-driver-security.test.mjs. It drives a real draft-75
upgrade through the installed driver's public Driver.server() API and
asserts observable behavior only, so it cannot report a security
regression because an internal field was renamed.

Seven suites, 15 leaf tests:

  pin    the version floor at 0.7.5, one flat lockfile copy, and the
         ranges sockjs and faye-websocket declare
  A      a length header of 20, 60 and 200 continuation bytes is refused
  B      a valid frame that follows such a header never reaches the app
  C      two normal small text frames still arrive as hello and world
  D      a body of exactly the budget is served, one byte over is refused
  E      header, varint terminator and a valid frame in one segment
  F      the 0x3ffffff default sockjs actually gets, bracketed from both
         sides: 4096 served, exactly-at-default served, one over refused

The guard fires inside the per-byte callback StreamReader.eachByte
drives, so the accumulator still overruns on a patched driver and the
connection is closed instead. The check asserts the close, not a small
number, which is the only defensible way to write it. An exception
escaping parse() counts as a failure, not as a rejection: a crash is the
parser losing control, and on 0.7.4 cases B and E do exactly that.

Measured on both versions by resolving each in a project tree and
re-running the file. 0.7.4: 9 of 15 fail, and no case that checks an
oversized header passes. 0.7.5: 15 of 15 pass. websocket-driver absent
from the tree: all 7 suites report skipped and the run exits 0, because
the package is transitive and a future webpack-dev-server or sockjs
upgrade can drop it. Node 20, which CI pins, agrees on all three.

The file is named *.test.mjs so `node --test tests/` discovers it, which
means the existing `npm test` step in ci-cd.yml runs it with no new
script and no CI edit. That keeps the js-yaml and shell-quote suites
added by #38 and #42 in place, and follows the harness they use.
SeriousJul added a commit that referenced this pull request Sep 25, 2026
Add tests/websocket-driver-security.test.mjs. It drives a real draft-75
upgrade through the installed driver's public Driver.server() API and
asserts observable behavior only, so it cannot report a security
regression because an internal field was renamed.

Seven suites, 15 leaf tests:

  pin    the version floor at 0.7.5, one flat lockfile copy, and the
         ranges sockjs and faye-websocket declare
  A      a length header of 20, 60 and 200 continuation bytes is refused
  B      a valid frame that follows such a header never reaches the app
  C      two normal small text frames still arrive as hello and world
  D      a body of exactly the budget is served, one byte over is refused
  E      header, varint terminator and a valid frame in one segment
  F      the 0x3ffffff default sockjs actually gets, bracketed from both
         sides: 4096 served, exactly-at-default served, one over refused

The guard fires inside the per-byte callback StreamReader.eachByte
drives, so the accumulator still overruns on a patched driver and the
connection is closed instead. The check asserts the close, not a small
number, which is the only defensible way to write it. An exception
escaping parse() counts as a failure, not as a rejection: a crash is the
parser losing control, and on 0.7.4 cases B and E do exactly that.

Measured on both versions by resolving each in a project tree and
re-running the file. 0.7.4: 9 of 15 fail, and no case that checks an
oversized header passes. 0.7.5: 15 of 15 pass. websocket-driver absent
from the tree: all 7 suites report skipped and the run exits 0, because
the package is transitive and a future webpack-dev-server or sockjs
upgrade can drop it. Node 20, which CI pins, agrees on all three.

The file is named *.test.mjs so `node --test tests/` discovers it, which
means the existing `npm test` step in ci-cd.yml runs it with no new
script and no CI edit. That keeps the js-yaml and shell-quote suites
added by #38 and #42 in place, and follows the harness they use.
SeriousJul added a commit that referenced this pull request Oct 6, 2026
#37)

* deps(deps): bump websocket-driver from 0.7.4 to 0.7.5

Resolve Dependabot alert #13, CVE-2026-54466. The draft-75 and draft-76
frame parsers in websocket-driver accumulate a base-128 length header
without bound. A client sends an unlimited run of bytes with the high bit
set, the accumulator grows past Number.MAX_SAFE_INTEGER, loses precision,
and later payload bytes are parsed against a wrong length. Message
content is then corrupted or dropped.

Version 0.7.5 rejects the message when the declared length passes the
configured maximum message length. The same release adds a payload check
in hybi.js _emitMessage, which clears alert #14 as well, CVE-2026-54490,
resource limit bypass via message compression. Both advisories list
0.7.5 as the first patched version.

websocket-driver is a transitive dependency. It is not named in
package.json, so only package-lock.json changes. The path is:

  @docusaurus/core -> webpack-dev-server -> sockjs -> websocket-driver

Both dependents already accept 0.7.5. sockjs asks for ^0.7.4 and
faye-websocket asks for >=0.5.1, so the single hoisted lockfile entry
moves and nothing else has to change. The whole diff is three lines:
version, resolved and integrity.

* test(deps): add a node:test regression check for CVE-2026-54466

Add tests/websocket-driver-security.test.mjs. It drives a real draft-75
upgrade through the installed driver's public Driver.server() API and
asserts observable behavior only, so it cannot report a security
regression because an internal field was renamed.

Seven suites, 15 leaf tests:

  pin    the version floor at 0.7.5, one flat lockfile copy, and the
         ranges sockjs and faye-websocket declare
  A      a length header of 20, 60 and 200 continuation bytes is refused
  B      a valid frame that follows such a header never reaches the app
  C      two normal small text frames still arrive as hello and world
  D      a body of exactly the budget is served, one byte over is refused
  E      header, varint terminator and a valid frame in one segment
  F      the 0x3ffffff default sockjs actually gets, bracketed from both
         sides: 4096 served, exactly-at-default served, one over refused

The guard fires inside the per-byte callback StreamReader.eachByte
drives, so the accumulator still overruns on a patched driver and the
connection is closed instead. The check asserts the close, not a small
number, which is the only defensible way to write it. An exception
escaping parse() counts as a failure, not as a rejection: a crash is the
parser losing control, and on 0.7.4 cases B and E do exactly that.

Measured on both versions by resolving each in a project tree and
re-running the file. 0.7.4: 9 of 15 fail, and no case that checks an
oversized header passes. 0.7.5: 15 of 15 pass. websocket-driver absent
from the tree: all 7 suites report skipped and the run exits 0, because
the package is transitive and a future webpack-dev-server or sockjs
upgrade can drop it. Node 20, which CI pins, agrees on all three.

The file is named *.test.mjs so `node --test tests/` discovers it, which
means the existing `npm test` step in ci-cd.yml runs it with no new
script and no CI edit. That keeps the js-yaml and shell-quote suites
added by #38 and #42 in place, and follows the harness they use.
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