Repository navigation
CVE-2026-104849: Tinypool: Prototype Pollution Gadget to RCE in run() options - #50
Merged
SeriousJul merged 3 commits intoOct 6, 2026
Conversation
tinypool 1.1.1 (transitive dep of @docusaurus/core 3.10.2) is vulnerable to prototype pollution to RCE via pool.run() options. No stable docusaurus release depends on patched tinypool, and the declared range ^1.0.2 cannot reach 2.x, so force tinypool 2.1.2 (first patched version) with an npm override. Verified: docusaurus SSG thread pool works with 2.1.2 (builds pass with DOCUSAURUS_SSG_WORKER_THREAD_COUNT=2), npm test 91/91 pass, typecheck clean. Alert: https://github.com/SeriousJul/seriousjul.github.io/security/dependabot/59
SeriousJul
marked this pull request as ready for review
October 6, 2026 22:40
Owner
Author
|
Score: 85 / 100 Specification Check: Pass
Quality Check: Pass, one gap
Required Changes:
|
Owner
Author
|
Added the missing regression test from the review. Changes (commit
What the test checks
Both states measured, as the review asked
Suite status after the change
|
SeriousJul
commented
Oct 6, 2026
SeriousJul
left a comment
Owner
Author
There was a problem hiding this comment.
Score: 94/100
Specification check: Pass
- Alert 59 (
tinypool < 2.1.2, transitive via@docusaurus/core) is resolved.npm ciinstalls exactly tinypool 2.1.2, andnpm auditno longer flags tinypool. - The cross-major override is the right mechanism.
@docusaurus/coredeclares^1.0.2, which cannot reach 2.1.2, and no stable docusaurus release depends on a patched tinypool. The commit message states this. - API compatibility verified. Every option docusaurus passes to the
Tinypoolconstructor (filename,minThreads,maxThreads,concurrentTasksPerWorker,runtime,isolateWorkers,workerData,maxMemoryLimitBeforeRecycle) still exists in 2.1.2. - End-to-end verified.
npm run buildpasses withDOCUSAURUS_SSG_WORKER_THREAD_COUNT=2, which forces the pooled SSG executor. That executor is the only tinypool consumer in the tree. - The regression test is empirically red on 1.1.1: with
Object.prototype.filenamepolluted,run(task, { signal })redirects the task to the attacker worker, exactly the advisory shape. On 2.1.2 all 7 tests pass. With tinypool absent, the suites skip with a reason and the exit code is 0. - Full suite: 98/98 pass. Typecheck clean.
Quality check: Pass
- Strong test design. The behaviour suite separates the vulnerable shape (caller-supplied options with no own
filename) from the safe no-options shape and the documented-API shape (explicit ownfilenameas negative control). The lockfile suite pins the version floor, asserts one flat copy matching the installed tree, and asserts the override exists and sits at or above the floor. - No new dependencies. The test uses
node:test. It runs in CI (ci-cd.ymlrunsnpm teston Node 20.x, which satisfies the 2.1.2 engines field). - The large lockfile diff against
maincomes from the stacked PRs #48/#49. The two commits of this PR change only the tinypool lock entry, the override, and the test files.
Non-blocking notes:
satisfies()reads an exact range (for example1.0.2) as a floor. The correct semantics for an exact range is equality. No impact in this tree, because only^1.0.2occurs.- When tinypool is absent, the lockfile-only checks skip too, although they read only JSON files and need no installed package. This is a documented tradeoff, and it is acceptable.
- The exact pin to 2.1.2 needs a dependabot or manual bump when tinypool ships a 2.x security fix. Version 2.2.0 already exists. Pining the first patched version is acceptable for a security fix.
Required changes: none
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://github.com/SeriousJul/seriousjul.github.io/security/dependabot/59
Package: tinypool
Ecosystem: npm
Vulnerable range: < 2.1.2
Severity: critical
CVSS score: 0
tinypoolis a fork ofpiscinaand inherited the same prototype-pollution surface. Whenpool.run(task, options)is called, thefilenameoption is read from the providedoptionsobject. If that object does not have an ownfilenameproperty, the lookup falls through toObject.prototype.An attacker who can pollute
Object.prototype.filename(for example, via a vulnerablelodash.merge,qs.parse, or similar elsewhere in the application) can make tinypool load and execute an attacker-controlled worker module.This is the tinypool counterpart to the piscina root discovery GHSA-x9g3-xrwr-cwfg.
pool.run(task)with no second argument is not affected, becausekDefaultOptions.filenameisnulland the options object is not user-controlled. The exploit only triggers when the caller passes their own options object topool.run().Impact
Arbitrary JavaScript execution in the worker pool. If the application passes attacker-controlled data as the
run()task and also supplies arun()options object, the attacker can redirect execution to a malicious worker that exfiltrates or modifies that data, achieving remote code execution and/or data exfiltration.Proof of Concept
Suggested fix
Read all user-supplied options from own properties only (
Object.hasOwnorObject.prototype.hasOwnProperty.call) and build the internalThreadPool.optionsobject with a null prototype.