[Security] Example frontends carry an unmaintained second lockfile: Dependabot patches package-lock.json while bun.lock keeps the vulnerable version #1402
Description
What
The eight bundled example frontends each carry two lockfiles —
package-lock.json and bun.lock — but only the npm one is ever
installed by CI, and Dependabot's npm updater only ever rewrites the npm
one. The bun half therefore drifts silently, and a merged security bump
leaves the directory half-patched.
#1401 is the worked example. It bumped fast-uri 3.1.5 → 3.1.7 (two
waves of high-severity advisories: authority injection via an unvalidated
port, host confusion via IP-literal brackets, SSRF via IPv6 normalization
and repeated percent-decoding). After that merge:
| directory | package-lock.json |
bun.lock |
|---|---|---|
examples/chat/frontend-angular |
3.1.7 ✅ | 3.1.0 ❌ |
examples/voice/frontend-angular |
3.1.7 ✅ | 3.1.2 ❌ |
Why nothing caught it
.github/workflows/examples.yml's frontend job installs with npm ci
(deliberately ci, not install, so a manifest/lockfile desync is an
error) and builds — from package-lock.json. Nothing anywhere installs
the example bun.lock files, so no gate can see them diverge. The
workflow header already documents the sibling failure mode that actually
happened (ts-pattern declared in six package.json files and missing
from all six package-lock.json files, unnoticed because nothing
installed them); this is the same class of defect, one lockfile over.
A second desync found while fixing it
Re-resolving the two bun lockfiles surfaced a pre-existing violation
unrelated to fast-uri: both package.json files declare
"overrides": {
"@hono/node-server": "^2.1.0",
"@modelcontextprotocol/sdk": "^1.30.0"
}while the committed bun.lock pinned @hono/node-server@1.19.14 and
@modelcontextprotocol/sdk@1.29.0 — i.e. the lockfile contradicted the
declared override. package-lock.json had already resolved them
correctly (2.1.0 / 1.30.0), which is further evidence that the bun half
is the unmaintained one.
Fix
Immediate: sync both bun.lock files (bun update fast-uri --lockfile-only in each directory), which also records the overrides and
converges the two lockfiles.
Structural (the part worth discussing): the drift can recur on the next
Dependabot bump. Options, roughly in increasing cost —
- Add a
bun install --frozen-lockfileleg to thefrontendmatrix, so
a desynced examplebun.lockis a red check rather than an invisible
one. Cheapest, and it mirrors what the npm leg already does. - Drop the example
bun.lockfiles entirely and let the frontends be
npm-only, since that is what CI and the documented build path use. - Keep both and add a periodic sync job.
(1) or (2) both close the gap; (3) does not, it only shortens the window.
Refs #1401. Related: #779 (the same package is vulnerable in the root
bun.lock through fastify > fast-json-stringify > ajv, inside the
runtime closure — a separate and worse exposure that Dependabot is blind
to by design).
Activity
pathosDev commentedon Sep 3, 2026
Option 1 is implemented, and measuring it corrected the issue body
I wrote option 1 above as if a
bun install --frozen-lockfileleg wouldclose this gap. It does not, and the difference is worth recording
before anyone treats the box as ticked.
Measured with
bun install --frozen-lockfile --dry-runagainst tworeconstructed states of
examples/chat/frontend-angular:fast-uri3.1.0 and the override violationnote: overrides in package.json changed since bun.lock was savedfast-uri3.1.0 alone, overrides already correctedSo the check validates manifest ↔ lockfile consistency, and
fast-uriappears in no
package.jsonat all — 3.1.0 satisfies the^3.0.1thatajvasks for, so a stale-but-in-range transitive dependency isinvisible to it.
The leg would therefore not, on its own, have caught #1401. On the
tree as it actually stood it would have gone red — but through the
unrelated override violation described above, not through
fast-uri.Once that violation is fixed, the next npm-only security bump passes it
silently.
What landed anyway, and why
The leg is still worth having: it closes a real class that had no
coverage at all (a
bun.lockcontradicting its own manifest), it ischeap, and it mirrors what the npm leg already does. It runs before the
Node setup so a desync is reported without paying for an install and a
build, and it was verified green against all eight matrix directories
first, so it does not arrive red. The measured limitation is written
into the step's comment rather than left to be rediscovered.
What is still open
Closing the class #1401 actually belongs to needs a cross-lockfile
version-map comparison — parse
package-lock.jsonandbun.lockinthe same directory and fail where the resolved version of a shared
package disagrees. One caveat found while looking at it: the two
resolvers legitimately land on different patches (npm has
@hono/node-server@2.1.0, bun2.1.1, both satisfying^2.1.0), so astrict equality check would be noisy as written and needs a deliberate
rule about what counts as disagreement.
Option 2 — dropping the example
bun.lockfiles entirely, sincenpm ciis what CI and the documented build path use — remains the cheaper way to
make the whole question go away, and is not foreclosed by this change.
Keeping this open for that decision.
chore(deps): sync the Angular example bun.lock files to fast-uri 3.1.7
ci(examples): gate the example bun.lock files, which nothing installed
Merge branch 'features/example-bun-lock-fast-uri' into develop
pathosDev commentedon Sep 10, 2026
The drift recurred on the very next Dependabot bump, and it is wider than fast-uri was
#1518 (hono 4.13.0 → 4.13.7, four advisories including a
hono/jsxXSS; plus next, sharp andbaseline-browser-mapping) landed the same way #1401 did — npm half patched, bun half untouched:
package-lock.jsonbun.lockbeforeexamples/chat/frontend-angularexamples/voice/frontend-angularSynced in the 2026-09 dependency sweep with
bun update hono baseline-browser-mapping --lockfile-only, the same recipe as246bbb9d.baseline-browser-mappingwas stale in both too(2.10.24 / 2.10.27 against 2.11.21).
Worth noting for option 1: the frozen-lockfile leg from
dc766f27is in place and green — Iran
bun install --frozen-lockfile --dry-runin all eight directories and all eight pass, bothbefore and after the sync. It cannot see this class of defect, because it compares a
bun.lockagainst the
package.jsonbeside it, and a stale transitive contradicts neither. The gate isworth having for what it does catch; it is not the answer to this issue.
How wide the divergence actually is
Since the sweep regenerated every lockfile anyway, I measured the full cross-lockfile agreement —
every package present in both files, compared by resolved version. Six of eight directories
disagree, and this is after both lockfiles were freshly regenerated:
Some of it crosses a major:
@inquirer/corenpm 12.0.3 against bun 11.2.1,magic-stringnpm1.3.1 against bun 0.30.21,
convert-source-mapnpm 2.0.0 against bun 1.9.0. And it is notone-directional —
react/react-domresolve to 19.2.8 under npm and 19.3.0 under bun, so thebun half is ahead there while it is behind elsewhere.
That reframes the structural question. This is not only "the bun half goes stale because nobody
updates it": the two resolvers do not converge even when both are regenerated from the same
manifest in the same minute.
bun install --lockfile-onlypreserves an existing resolution whilethe range still admits it;
npm install --package-lock-onlyre-resolves. So no discipline of"remember to update both" produces two files that agree — which is an argument for option 2
(drop the example
bun.lockfiles, since npm is what CI and the documented build path use)rather than for any amount of syncing.
Until that is decided, the sweep's position is the same as
246bbb9d's: sync what issecurity-relevant, leave the rest, and record the measurement rather than quietly re-resolving 54
packages in an example frontend.
The eight frontends were also bumped in that sweep (Angular 22.1, next 16.3.4, react 19.2.8, vite
8.2.2, svelte 5.57, zone.js 0.16), and one of each toolchain was verified end to end with
npm ciplus a real build. Refs #1520.
chore(deps): sweep the eight example frontends, and finish #1518's bu…
ci: watch every package.json with Dependabot, through the updater tha…
chore(examples): sync the eight frontend bun.lock files with the Depe…
chore(examples): sync the two Next frontends' bun.lock with the @type…