Skip to content

[Security] Example frontends carry an unmaintained second lockfile: Dependabot patches package-lock.json while bun.lock keeps the vulnerable version #1402

Open

Description

@pathosDev

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 —

  1. Add a bun install --frozen-lockfile leg to the frontend matrix, so
    a desynced example bun.lock is a red check rather than an invisible
    one. Cheapest, and it mirrors what the npm leg already does.
  2. Drop the example bun.lock files entirely and let the frontends be
    npm-only, since that is what CI and the documented build path use.
  3. 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

  1. added
    infrastructureCI / build / live-integration tests
    dependenciesPull requests that update a dependency file
    securitySecurity-relevant — see severity label for impact tier
    severity: mediumModerate impact or requires specific conditions
    on Sep 3, 2026
  2. pathosDev commented on Sep 3, 2026

    @pathosDev
    OwnerAuthor

    Option 1 is implemented, and measuring it corrected the issue body

    I wrote option 1 above as if a bun install --frozen-lockfile leg would
    close 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-run against two
    reconstructed states of examples/chat/frontend-angular:

    state result
    pre-fix: fast-uri 3.1.0 and the override violation exit 1 — note: overrides in package.json changed since bun.lock was saved
    fast-uri 3.1.0 alone, overrides already corrected exit 0

    So the check validates manifest ↔ lockfile consistency, and fast-uri
    appears in no package.json at all — 3.1.0 satisfies the ^3.0.1 that
    ajv asks for, so a stale-but-in-range transitive dependency is
    invisible 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.lock contradicting its own manifest), it is
    cheap, 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.json and bun.lock in
    the 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, bun 2.1.1, both satisfying ^2.1.0), so a
    strict equality check would be noisy as written and needs a deliberate
    rule about what counts as disagreement.

    Option 2 — dropping the example bun.lock files entirely, since npm ci
    is 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.

  3. pathosDev commented on Sep 10, 2026

    @pathosDev
    OwnerAuthor

    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/jsx XSS; plus next, sharp and
    baseline-browser-mapping) landed the same way #1401 did — npm half patched, bun half untouched:

    directory package-lock.json bun.lock before after
    examples/chat/frontend-angular hono 4.13.7 4.12.16 4.13.7
    examples/voice/frontend-angular hono 4.13.7 4.12.17 4.13.7

    Synced in the 2026-09 dependency sweep with bun update hono baseline-browser-mapping --lockfile-only, the same recipe as 246bbb9d. baseline-browser-mapping was stale in both too
    (2.10.24 / 2.10.27 against 2.11.21).

    Worth noting for option 1: the frozen-lockfile leg from dc766f27 is in place and green — I
    ran bun install --frozen-lockfile --dry-run in all eight directories and all eight pass, both
    before and after the sync. It cannot see this class of defect, because it compares a bun.lock
    against the package.json beside it, and a stale transitive contradicts neither. The gate is
    worth 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:

    directory shared packages disagreeing
    chat/frontend-angular 392 54
    voice/frontend-angular 392 54
    chat/frontend-next 61 4
    voice/frontend-next 61 4
    chat/frontend-svelte 82 3
    voice/frontend-svelte 82 3
    chat/frontend-react 49 0
    voice/frontend-react 49 0

    Some of it crosses a major: @inquirer/core npm 12.0.3 against bun 11.2.1, magic-string npm
    1.3.1 against bun 0.30.21, convert-source-map npm 2.0.0 against bun 1.9.0. And it is not
    one-directional — react/react-dom resolve to 19.2.8 under npm and 19.3.0 under bun, so the
    bun 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-only preserves an existing resolution while
    the range still admits it; npm install --package-lock-only re-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.lock files, 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 is
    security-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 ci
    plus a real build. Refs #1520.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    dependenciesPull requests that update a dependency fileinfrastructureCI / build / live-integration testspriority: mediumUseful, not urgentsecuritySecurity-relevant — see severity label for impact tierseverity: mediumModerate impact or requires specific conditions

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions