No local patches to third-party dependencies
Two advisories had no upstream fix, and the first answer was a local patch: full
copies of braces and http-cache-semantics under vendor/, wired in with
file: dependencies and overrides. That removed the changed source from the
dependency graph rather than proving it safe, and it left this repository
carrying third-party source it does not own. We decided this project never
patches third-party source locally. When an advisory has no upstream fix, the
response is a reachability analysis, a registry version pin where one exists, and
replacement of the dependency or of the code path that reaches it when the
dependency is unhealthy.
Why the local patch was wrong
The advisory ranges were braces <= 3.0.3 (CVE-2026-93687,
first_patched_version: null) and http-cache-semantics <= 4.2.0
(CVE-2026-93748, disputed upstream, first_patched_version: null). The vendored
copies declared versions 3.0.4 and 4.3.1, and Dependabot alerts #57 and #58 moved
to fixed. They moved because the package names left the graph, not because
GitHub saw a patched package. The repository SBOM reported
pkg:npm/vendor/braces@3.0.4 and pkg:npm/vendor/http-cache-semantics@4.3.1,
held no braces entry at all, and carried no license data for either copy. A
scan of the shipped tree could no longer see what the project actually runs.
npm ci also fails late when a local path dependency is missing. With the
file: target absent, npm ci exits 0 and leaves a dangling symlink in
node_modules; the build then dies with MODULE_NOT_FOUND. A committed
directory that a lockfile depends on is a silent single point of failure.
Considered options
- Keep the patched copies in this repository, with provenance and expiry checks. Rejected: it keeps changed third-party source in the dependency path, and the graph blindness stays.
- Move the patched copies to a separate repository and consume them by git URL or tarball. Rejected: same deviation, plus a second repository and a cross-repo release step for two patches.
- Commit
patches/*.patchand apply them withpatch-packageonpostinstall. Rejected: the lockfile keepsbraces@3.0.3, so the advisory stays open, and apostinstallhook adds an install-time dependency of its own. - Accept the advisories on the grounds that both packages are toolchain dependencies. Rejected as a reason: toolchain-only use is not the argument. The argument is that the project does not qualify. See the analyses below.
Reachability: this project does not qualify
These analyses are the evidence behind the Dependabot dismissals.
braces, CVE-2026-93687. The vulnerable code is the recursive AST walkers
(compile, expand, stringify), which had no depth guard. Patterns reach
braces only through micromatch (used by globby and by
@docusaurus/utils globUtils.createMatcher, which calls
Micromatch.makeRe(pattern)) and through chokidar. Both receive patterns
authored in this repository's committed configuration. File paths are the strings
being matched; they are never parsed as patterns. The attack needs a pattern of
about 9999 characters nested 4999 levels deep. A path component on the CI
filesystem is at most 255 bytes, and no committed pattern in this repository is
near that length. No outside party can supply a pattern to this build.
http-cache-semantics, CVE-2026-93748. The package is reached only through
@docusaurus/core bin/beforeCli.mjs to update-notifier to latest-version
to package-json to got to cacheable-request. The advisory describes a
shared cache that returns one user's Set-Cookie to another user through a
max-stale request. This project has one local user and one local cache file at
~/.config/configstore/update-notifier-@docusaurus. There is no shared cache and
no multi-user serving. update-notifier also disables itself when
NO_UPDATE_NOTIFIER is present in the environment, so CI sets that variable and
the code path never runs there.
Consequences
bracesresolves back to the registry 3.0.3, which is inside the advisory range, so its Dependabot alert reopens. It is dismissed with reasonnot_usedand a comment that cites this ADR.http-cache-semanticsresolves to the registry 4.3.0, which is outside the advisory range<= 4.2.0, so it raises no alert. The behaviour the disputed advisory describes is unchanged in 4.3.0; the dismissal rests on the analysis above, not on the version.- The dependency graph and SBOM name the real packages again.
tests/dependency-policy.test.mjsfails the build when a dependency spec is anything other than a plain registry semver range, or when a path undervendor/is tracked. Git URLs and tarball URLs are local patches parked elsewhere, so they fail too. Adding a fork dependency requires a new ADR.- Registry version pins that already exist (
postcss-selector-parser,tinypool) stay. They select published registry versions, so they are version selection, not patching. - Exit condition: when upstream publishes a fix, Dependabot security updates open the pull request on their own. When an advisory stays open with no upstream fix, the response is the reachability analysis, a registry pin where one exists, and an evaluation of a replacement dependency at the next dependency review.