---
title: "Transitive CVE Clearance: The Dual-Layer Pattern"
description: "How v0.9.1 cleared 6 high-severity transitive CVEs in axios + fast-uri — and why the dep bump needed top-level overrides to actually stick."
date: "2026-05-13"
tags: ["security", "node", "dependency-management", "bun", "claude-code-slack-channel", "release-engineering"]
featured: false
---

You bump a direct dependency to pull in a patched transitive. `bun audit` goes green. The lockfile is committed. Two weeks later, someone does a clean install on a fresh machine, and the vulnerable transitive comes back. This is the transitive CVE trap, and it catches teams with the first move alone.

The v0.9.1 release of claude-code-slack-channel cleared 6 high-severity CVEs in axios and fast-uri. It required two distinct moves: first, bump the direct deps that pull the patched transitives. Second, pin those transitives at the top-level overrides block so the lockfile cannot regress on the next `bun install`. Both moves are mandatory. Here's why.

## The CVE Picture

Six vulnerabilities came down from the audit:

**axios** (multiple prototype-pollution and header-injection chains):
- GHSA-q8qp-cvcw-x6jj — credential injection via prototype pollution
- GHSA-pmwg-cvhr-8vh7 — NO_PROXY bypass via 127.0.0.0/8
- GHSA-6chq-wfr3-2hj9 — header injection through polluted properties
- GHSA-pf86-5x62-jrwf — response-tampering gadgets in prototype chain

**fast-uri** (percent-encoding confusion):
- GHSA-v39h-62p7-jpjc — host confusion via percent-encoded delimiters
- GHSA-q3j6-qgpj-74h6 — path traversal via percent-encoded dot segments

All were high severity. Axios was reachable through `@slack/web-api`, and fast-uri through `@modelcontextprotocol/sdk`.

## Move 1: Bump the Direct Deps

The straightforward path: bump the deps that pull the patched versions.

```
@slack/web-api 7.15.0 → 7.15.2    (pulls axios ^1.13.5 → ^1.15.0, resolves to 1.16.1)
@modelcontextprotocol/sdk 1.27.1 → 1.29.0    (refreshes ajv → fast-uri 3.1.2)
```

Commit this, run the lockfile lock, `bun audit` shows green. Done, right?

Not quite.

## The Lockfile Trap

Package managers use semantic versioning ranges. `@slack/web-api` at 7.15.2 declares `axios ^1.15.0`, which matches 1.15.x, 1.16.x, and newer. The first install on your CI or contributor's machine might pull 1.16.1 (the patched version). But six months later, when the MCP SDK maintainer releases a new version that also depends on axios *with a different range* like `^1.13.0`, and a contributor runs `bun install` on a fresh checkout without the lockfile, the resolver has two legitimate paths to axios: one through Slack at 1.16.1 and one through MCP at 1.13.x. Package managers are free to choose — and if they pick the older one, the CVE is back.

The lockfile prevents this *within a known tree*, but it has a shelf life. Lockfiles can be ignored (clean install), overridden (manual dependency update), or corrupted (merge conflicts). The real guard is a top-level override that says: "No matter what ranges the transitives declare, axios stays at ^1.16.1 and fast-uri stays at ^3.1.2, always."

## Move 2: Pin at the Top-Level Overrides Block

In Bun (and npm/yarn with overrides support), you declare a top-level policy:

```json
{
  "dependencies": {
    "@slack/web-api": "7.15.2",
    "@modelcontextprotocol/sdk": "1.29.0"
  },
  "overrides": {
    "axios": "^1.16.1",
    "fast-uri": "^3.1.2"
  }
}
```

The `overrides` block forces every transitive reference to those packages to resolve through the pinned versions, *regardless of what ranges the direct deps declare*. Now a future lockfile, a fresh install, a contributor on a different machine — all of them get the patched versions. The CVE cannot re-emerge through a range mismatch.

Without the override, the next `bun install` on a clean tree could legally pull axios 1.13.x (or whatever version a new transitive path declares) and the CVE is back. With the override, it cannot.

## Why Both Moves Matter

Move 1 (the dep bump) gets the patched version into the lockfile the first time and signals intent to the dependency tree. Move 2 (the override) is the insurance policy — it says "this version is non-negotiable" to any future resolver, whether it's a clean install, a new team member, or a GitHub Actions runner months from now.

Neither move alone is complete. Bump without override = fragile; override without bump = signals a different problem (the direct dep is stale and needs its own fix). Both together = the CVE cannot come back.

## Evidence: The Full Gauntlet

The release ran the Intent Solutions testing gauntlet on every change:

- **704/704 tests passing** (unit + integration + system + E2E)
- **98.47% line coverage, 98.82% function coverage** (floor enforced by CI gate)
- **Cyclomatic complexity max = 28** (threshold = 30, no violations)
- **Harness-hash integrity verified** (test policy signatures unchanged)
- **Depcruise clean** (dependency graph validated, no cycles, no forbidden imports)
- **Gherkin-lint clean** (all acceptance test syntax valid)
- **bun audit --audit-level=high clean** (excluding one known unpatched transitive marked safe by policy)

A version bump that doesn't clear the full gauntlet doesn't ship. This one did.

## Parallel Work in the Same Release Window

PR #162 (external contributor @PGMacDesign) fixed the file-upload extension bug — uploads were defaulting to `file.txt` because the filename wasn't being passed to `filesUploadV2`. That change rode the same release vehicle, showing the dual-layer pattern applies to all release-critical fixes, not just CVEs.

PR #164 cleaned up documentation drift after the CVE work landed — updated CLAUDE.md cross-references, dropped the gemini-review workflow (now handled via GitHub App), refreshed the source file LoC table to match the 704-test count, and softened coverage claims to "~704 / ~4,035" with a note that the floor is the real gate, not the count.

All three PRs (#162, #163, #164) merged into a single release tag with a 157-line AAR documenting the bump rationale, the CVE IDs, the test results, and the decision to include the external contribution in the same release window.

## Takeaway

Clearing a transitive CVE is not a one-move operation. Bump the direct dep, run the gauntlet, add the top-level override, and commit both. The override is the difference between a fix that sticks and a fix that waits for the next fresh install to fail.

## Related Posts

- [CCSC: Five Releases in One Day — Security Sprint](/posts/ccsc-five-releases-one-day-security-sprint/) — the prior security sprint on the same repo, where the v0.8.x baseline got hardened before this v0.9.1 patch.
- [Slack Channel Security Hardening v0.2.0 — External Contributors](/posts/slack-channel-security-hardening-v020-external-contributors/) — earlier hardening pass plus an external-contributor merge story, parallel to today's #162.
- [Audit Harness v0.1.0 — Enforcement Travels with the Code](/posts/audit-harness-v010-enforcement-travels-with-code/) — the vendored gauntlet (`.audit-harness/`) that produced the 704/704 + 98.47% evidence in this release.
