---
title: 'The Bun CVE Gap, Closed: Surgical Updates Land in Bun 1.4'
publishedAt: '2026-08-21T12:00:00Z'
summary: "Three months ago, I wrote that Bun was the only mainstream package manager that couldn't do surgical CVE remediation. Bun 1.4 ships in-place transitive updates, bun audit fix, and nested overrides. Time to re-run the original reproductions."
tags: ['dependencies', 'security', 'tooling', 'bun']
series: 'CVE Hygiene'
---

This is the fourth installment of what is now officially a series about CVE hygiene, and it's the one I was hoping I'd get to write. First, [the workflow](https://charpeni.com/blog/minimizing-risk-properly-and-safely-resolving-cves-in-your-dependencies): resolve CVEs with the smallest possible diff, and only reach for `resolutions`/`overrides` as a last resort. Then, [the Bun CVE gap](https://charpeni.com/blog/the-bun-cve-gap-when-your-package-manager-cant-do-surgical-updates): Bun was the only mainstream JavaScript package manager that couldn't do any of it. Most recently, [pnpm's `audit --fix`](https://charpeni.com/blog/surgical-transitive-updates-with-pnpm-audit-fix): pnpm automating the entire workflow better than anyone.

[Bun 1.4 shipped yesterday](https://bun.com/blog/bun-v1.4), and it closes the gap: in-place transitive updates are now the **default behavior** of `bun update`, there's a proper `bun audit fix`, and overrides finally support nested and version-scoped forms. I re-ran the reproductions from the original post against 1.4.0 to see if the claims hold up.

> [!NOTE]
> Everything below was tested on Linux in August 2026 with **Bun 1.4.0**. The original post was based on Bun 1.3.14.

## From a closed PR to default behavior

At the end of the original post, I mentioned I had opened a pull request adding a `--transitive` flag to `bun update` so transitives could be re-resolved in place: [oven-sh/bun#31143](https://github.com/oven-sh/bun/pull/31143).

That PR is now closed, unmerged. And I couldn't be happier about it:

<div className="img-center">
  <Image
    alt={`Screenshot of a GitHub comment from Jarred Sumner closing the --transitive pull request in favor of making transitive updates the default behavior of bun update`}
    src={`https://charpeni.com/static/images/the-bun-cve-gap-closed-surgical-updates-land-in-bun-1-4/closing-comment.png`}
    width={2252}
    height={1252}
  />
</div>

It was superseded by [oven-sh/bun#38333](https://github.com/oven-sh/bun/pull/38333), which makes transitive updates the default behavior of `bun update` instead of an opt-in flag, and closes [the feature request](https://github.com/oven-sh/bun/issues/24523) that had been open since November 2025. I proposed a flag mostly to be conservative about changing existing behavior; the Bun team went further and made it the default, which is exactly where I hoped this would eventually land. Getting superseded by something strictly better, with credit in the description, is the good ending for a drive-by contribution.

The best part is the title of the PR that shipped all of this: _"install: **pnpm parity** — dedupe, prune, pm licenses, audit fix, add --filter/--catalog, nested overrides, transitive update, and workspace fixes"_. I closed the pnpm post by calling pnpm the package manager that's quietly becoming the best at this. Apparently the Bun team agrees: pnpm is now the yardstick.

> [!NOTE]
> Also, yes: the comment closing my PR was written by Claude, on behalf of the Bun team. Welcome to 2026.

## The new workflow

Before digging into the details, here's what CVE remediation looks like on Bun 1.4. The workflow from [the original post](https://charpeni.com/blog/minimizing-risk-properly-and-safely-resolving-cves-in-your-dependencies) (smallest possible diff first, overrides last) now maps directly onto Bun commands:

1. `bun audit` to see which advisories apply to your tree.
2. `bun audit fix --dry-run` to review the plan: what moves, what's blocked, and why.
3. `bun audit fix` to apply it, adding `--latest` only when a fix genuinely requires crossing a major version.
4. For a transitive you want to move yourself, `bun update <name>` re-resolves it in place, within every parent's range.
5. For anything that can't be fixed within ranges, a scoped override (nested and version-scoped forms both work now).

Then re-run `bun audit` to confirm, test your project, done. If that list sounds unremarkable, that's the point: it's the same workflow you'd use with Yarn Berry, pnpm, or npm.

## Wrapping up

Everything from the original series' checklist (update within range, then the parent, and only then a scoped override) now has a first-class Bun command behind it, and every reproduction that failed in 1.3 passes in 1.4.

Three months ago, Bun couldn't update a transitive dependency in place at all. Today, transitive updates are the default behavior of `bun update`, `bun audit fix` respects your pins, your ranges, and your release-age quarantine, and overrides can finally be scoped. One release, and Bun is suddenly near the front of the pack.

The Bun CVE gap is closed. And nobody has an excuse to delete `bun.lock` anymore. 🙂

The rest of this post is the receipts: the reproductions from the original post, re-run against 1.4.0. Let's dig in.

## Re-running the original reproduction

Same minimal setup as last time. `body-parser@1.20.0` transitively pins `qs` to `6.10.3`:

```json:package.json
{
  "name": "bun-transitive-test",
  "dependencies": {
    "body-parser": "1.20.0"
  }
}
```

```bash
bun install
bun why qs
# qs@6.10.3
#   └─ body-parser@1.20.0 (requires 6.10.3)
#      └─ bun-transitive-test (requires 1.20.0)
```

In 1.3.14, `bun update qs` silently added `qs` as a direct dependency and left the vulnerable transitive in place. In 1.4.0:

```bash
bun update qs
# bun update v1.4.0
# Checked 41 installs across 42 packages (no changes)
```

No phantom direct dependency, `package.json` untouched. And "no changes" is the honest answer here: `body-parser@1.20.0` pins `qs` to exactly `6.10.3`, so there is nothing to re-resolve within range.

## The in-place transitive update

Now the scenario the whole series is built around: a transitive that is genuinely stale within its parent's range. Same reproduction as the original post: `chokidar@3.5.0` depends on `braces ~3.0.2`, and I hand-edited `bun.lock` to downgrade `braces` from `3.0.3` to `3.0.2`. The lockfile says `3.0.2`, the parent's range would happily accept `3.0.3`.

In 1.3.14, no command re-resolved this within range (except a global override). In 1.4.0:

```bash
bun why braces
# braces@3.0.2
#   └─ chokidar@3.5.0 (requires ~3.0.2)

bun update braces
# ^ braces 3.0.2 -> 3.0.3
# 1 package installed
```

The transitive gets re-resolved in place, the parent stays put, `package.json` is untouched, and the diff is one line in the lockfile. This is `yarn up braces --recursive`, except it's just `bun update braces` now. The primitive I spent an entire post complaining about now exists.

A bare `bun update` does the same for every package in `bun.lock`, like `pnpm update` and `npm update`, and patterns work too:

```bash
bun update
bun update braces
bun update '@types/*'
```

> [!NOTE]
> One shape doesn't map one-to-one: `bun update chokidar` (the parent) still leaves chokidar's _own_ dependencies locked. If you want the transitive moved, name it directly or run a bare `bun update`.

## `bun audit fix`

Conveniently, my reproduction aged into a real-world example while I wasn't looking: `body-parser@1.20.0` and its pinned `qs@6.10.3` both picked up advisories since May.

```bash
bun audit
# body-parser@1.20.0
#   (direct dependency)
#   low: body-parser vulnerable to denial of service when invalid limit value silently disables size enforcement (<1.20.6)
#   high: body-parser vulnerable to denial of service when url encoding is enabled (<1.20.3)
#
# qs@6.10.3
#   body-parser > qs
#   low: qs's arrayLimit bypass in comma parsing allows denial of service (>=6.7.0 <=6.14.1)
#   moderate: qs's arrayLimit bypass in its bracket notation allows DoS via memory exhaustion (<6.14.1)
#
# 4 vulnerabilities (1 high, 1 moderate, 2 low)
```

Bun has had a read-only `bun audit` for a while. What's new in 1.4 is `bun audit fix`, and it starts with my favorite flag:

```bash
bun audit fix --dry-run
# fixing:
#   ^ body-parser 1.20.0 -> 1.20.6
#     package.json: 1.20.0 -> 1.20.6
#
# blocked by a dependent's range:
#   ^ qs 6.10.3 -> 6.14.2
#     body-parser@1.20.0 depends on qs@6.10.3
#
# Would fix 2 vulnerabilities in 1 package (checked 41)
# 2 vulnerabilities remaining
```

Three details in there are worth slowing down for:

1. **Pinned versions are handled the way you'd hope.** I pinned `body-parser` to exactly `1.20.0`, and the fix requires leaving that pin. So Bun rewrites `package.json` and says so, right in the plan: `package.json: 1.20.0 -> 1.20.6`. After applying, the pin is still a pin (`"1.20.6"`, not `"^1.20.6"`). Per the help text: _"package.json is only changed when an exact pin has to be bumped"_. Ranged dependencies get fixed in the lockfile without touching your manifest at all.
2. **It targets the lowest safe version, not the latest.** The proposed `qs` fix is `6.14.2`, the minimum version that resolves both advisories, not `6.15.3`, the newest release. That's the same principle I praised in pnpm's `minimumReleaseAgeExclude` design: pre-approve the version that fixes the problem, not whatever happens to be newest.
3. **Blocked fixes are reported, not fudged.** `qs` can't move because `body-parser@1.20.0` pins it exactly, and the output names the blocker instead of skipping it or force-overriding it.

Now, apply it:

```bash
bun audit fix
# Fixed 4 vulnerabilities in 1 package (checked 41)

bun why qs
# qs@6.15.3
#   └─ body-parser@1.20.6 (requires ~6.15.1)

bun audit
# No vulnerabilities found (checked 41 packages)
```

Wait, the dry run said it would fix 2 and leave 2 remaining, but the real run fixed all 4? That's the cascade: bumping `body-parser` to `1.20.6` changes its `qs` requirement to `~6.15.1`, which resolves to `6.15.3`, past both `qs` advisories. The dry run under-promises because it plans against the current tree; the real run re-audits the tree it actually installed and reports from that. I'll take a tool that surprises me in this direction.

When a fix needs a new major version, `bun audit fix` says so and refuses; `--latest` opts into rewriting your declared ranges to take it. There's also `--ignore GHSA-…` and `--audit-level` for CI exit-code control.

> [!TIP]
> The one thing I miss from pnpm's implementation is `--interactive`, to review and apply fixes one by one. The closest Bun equivalent is `bun audit fix --dry-run`, review the plan, then apply.

## The `minimumReleaseAge` interaction

In the pnpm post, I called the interaction between `audit --fix` and [`minimumReleaseAge`](https://charpeni.com/blog/protecting-against-compromised-packages-with-minimum-release-age) something I hadn't seen anywhere else. Well, now I've seen it somewhere else.

Bun 1.3 shipped `install.minimumReleaseAge` in `bunfig.toml` (in seconds), and in 1.4, security fixes bypass it, with an annotation. Here's the same reproduction with a quarantine window large enough to block the `body-parser` fix, which was 42 days old at the time of testing:

```toml:bunfig.toml
[install]
minimumReleaseAge = 4320000 # 50 days, exaggerated for the demo
```

```bash
bun audit fix
# fixing:
#   ^ body-parser 1.20.0 -> 1.20.6 (newer than --minimum-release-age)
#     package.json: 1.20.0 -> 1.20.6
```

The fix installs immediately, and the bypass is called out inline instead of happening silently.

The design is different from pnpm's, though. pnpm persists the exception: the patched version lands in `minimumReleaseAgeExclude` in your config, so the "why was the quarantine bypassed" context survives into the pull request. Bun's bypass is a one-time decision annotated in the command output: nothing is persisted, and there's nothing to clean up afterwards. The version change itself still shows up in your `package.json` and `bun.lock` diff either way. I still have a soft spot for pnpm's explicit, reviewable entry, but Bun gets the important part right: a known CVE fix shouldn't have to sit out a quarantine window that exists to protect you from unknown ones.

> [!NOTE]
> The bypass is scoped to the vulnerable package itself. If the patched release pulls in brand-new dependencies of its own that are also inside your window, those are still blocked and the fix fails with a resolution error. I could only trigger this with an absurdly large window (two years); with a realistic window of one to three days, you're unlikely to ever hit it.

## Nested overrides, at last

The other half of the original post was about Bun's overrides being keyed by package name only: every copy of a name gets redirected, and all the scoped syntaxes from other package managers either warned or silently did nothing.

In 1.4, they all just work. npm's nested form:

```json:package.json
{
  "name": "bun-nested-overrides-test",
  "dependencies": {
    "body-parser": "1.20.0",
    "qs": "6.9.0"
  },
  "overrides": {
    "body-parser": { "qs": "6.11.0" }
  }
}
```

```bash
bun why qs
# qs@6.9.0
#   └─ bun-nested-overrides-test (requires 6.9.0)
#
# qs@6.11.0
#   └─ body-parser@1.20.0 (requires 6.10.3)
```

Only `body-parser`'s copy of `qs` is redirected; the direct `qs@6.9.0` is left alone. That's the surgical override the original post was trying to express. Yarn's `"body-parser/qs"` and pnpm's `"body-parser>qs"` forms are accepted too, and produce the same result.

Version-scoped overrides also work now:

```json:package.json
{
  "overrides": {
    "qs@<6.11.0": "6.11.0"
  }
}
```

This is the version-keyed resolutions pattern from my original CVE post, the exact syntax that used to silently match nothing in Bun. Overrides are still the last-resort tool, and everything I've written about scoping and removing them still applies. The difference is that Bun now rarely forces you to reach for them, and when you do, they're finally expressive enough to be surgical.

## Also shipped

A few more `bun install` additions from 1.4 that fit the theme:

- [`bun dedupe`](https://bun.com/blog/bun-v1.4#bun-dedupe) collapses duplicate versions using only versions already in the lockfile, never touches `package.json`, and `--check` fails CI when duplicates exist.
- [`bun prune`](https://bun.com/blog/bun-v1.4#bun-prune) removes everything from `node_modules` that the lockfile doesn't put there.
- [`bun pm diff`](https://bun.com/blog/bun-v1.4#bun-pm-diff) shows what changed between two versions of a package, including new install scripts and new imports of `child_process`, `fs`, `net`, or `vm`. Handy for reviewing the updates you're about to take.
