Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
563fbbb416 fix(install): reject unsupported integrity hash algorithms in lockfiles
Previously, if a lockfile contained an integrity hash with an
unrecognized algorithm (e.g., "md5-AAAA"), the tag would parse as
unknown and the integrity verification would be silently skipped. This
meant a tampered lockfile could disable integrity checking entirely.

Now all lockfile parsers (bun.lock, yarn.lock, pnpm-lock.yaml,
package-lock.json) reject non-empty integrity strings with unsupported
hash algorithms. As defense-in-depth, the tarball extraction step also
errors when an npm package lacks a supported integrity hash.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 04:48:27 +00:00
6 changed files with 164 additions and 14 deletions

View File

@@ -11,13 +11,24 @@ url: strings.StringOrTinyString,
package_manager: *PackageManager,
pub inline fn run(this: *const ExtractTarball, log: *logger.Log, bytes: []const u8) !Install.ExtractData {
if (!this.skip_verify and this.integrity.tag.isSupported()) {
if (!this.integrity.verify(bytes)) {
if (!this.skip_verify) {
if (this.integrity.tag.isSupported()) {
if (!this.integrity.verify(bytes)) {
log.addErrorFmt(
null,
logger.Loc.Empty,
bun.default_allocator,
"Integrity check failed<r> for tarball: {s}",
.{this.name.slice()},
) catch unreachable;
return error.IntegrityCheckFailed;
}
} else if (this.resolution.tag == .npm) {
log.addErrorFmt(
null,
logger.Loc.Empty,
bun.default_allocator,
"Integrity check failed<r> for tarball: {s}",
"Missing or unsupported integrity hash for tarball: {s}",
.{this.name.slice()},
) catch unreachable;
return error.IntegrityCheckFailed;

View File

@@ -1868,6 +1868,10 @@ pub fn parseIntoBinaryLockfile(
};
pkg.meta.integrity = Integrity.parse(integrity_str);
if (integrity_str.len > 0 and !pkg.meta.integrity.tag.isSupported()) {
try log.addError(source, integrity_expr.loc, "Unsupported integrity hash algorithm");
return error.InvalidPackageInfo;
}
},
inline .git, .github => |tag| {
// .bun-tag

View File

@@ -541,13 +541,15 @@ pub fn migrateNPMLockfile(
.false;
} else .false,
.integrity = if (pkg.get("integrity")) |integrity|
Integrity.parse(
integrity.asString(this.allocator) orelse
return error.InvalidNPMLockfile,
)
else
Integrity{},
.integrity = if (pkg.get("integrity")) |integrity| blk: {
const integrity_str = integrity.asString(this.allocator) orelse
return error.InvalidNPMLockfile;
const parsed = Integrity.parse(integrity_str);
if (integrity_str.len > 0 and !parsed.tag.isSupported()) {
return error.InvalidNPMLockfile;
}
break :blk parsed;
} else Integrity{},
},
.bin = if (pkg.get("bin")) |bin| bin: {
// we already check these conditions during counting

View File

@@ -613,6 +613,9 @@ pub fn migratePnpmLockfile(
};
pkg.meta.integrity = Integrity.parse(integrity_str);
if (integrity_str.len > 0 and !pkg.meta.integrity.tag.isSupported()) {
return invalidPnpmLockfile();
}
}
}

View File

@@ -997,10 +997,14 @@ pub fn migrateYarnLockfile(
} else .all,
.man_dir = String{},
.has_install_script = .false,
.integrity = if (entry.integrity) |integrity|
Integrity.parse(integrity)
else
Integrity{},
.integrity = if (entry.integrity) |integrity| blk: {
const parsed = Integrity.parse(integrity);
if (integrity.len > 0 and !parsed.tag.isSupported()) {
try log.addError(null, logger.Loc.Empty, "Unsupported integrity hash algorithm in yarn.lock");
return error.InvalidYarnLock;
}
break :blk parsed;
} else Integrity{},
},
.bin = Bin.init(),
.scripts = .{},

View File

@@ -0,0 +1,126 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("lockfile with unsupported integrity hash algorithm should fail", async () => {
using dir = tempDir("unsupported-integrity", {
"package.json": JSON.stringify({
name: "test-unsupported-integrity",
dependencies: {
"is-number": "7.0.0",
},
}),
"bun.lock": JSON.stringify(
{
lockfileVersion: 1,
configVersion: 1,
workspaces: {
"": {
name: "test-unsupported-integrity",
dependencies: {
"is-number": "7.0.0",
},
},
},
packages: {
"is-number": ["is-number@7.0.0", "", {}, "md5-AAAAAAAAAA=="],
},
},
null,
2,
),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "install", "--frozen-lockfile"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("Unsupported integrity hash algorithm");
expect(exitCode).toBe(1);
});
test("lockfile with valid integrity hash algorithm should succeed", async () => {
// First, create a real lockfile by installing
using dir = tempDir("valid-integrity", {
"package.json": JSON.stringify({
name: "test-valid-integrity",
dependencies: {
"is-number": "7.0.0",
},
}),
});
// Run install to generate a valid lockfile
await using installProc = Bun.spawn({
cmd: [bunExe(), "install"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const installExitCode = await installProc.exited;
expect(installExitCode).toBe(0);
// Now run with --frozen-lockfile to verify it works
await using proc = Bun.spawn({
cmd: [bunExe(), "install", "--frozen-lockfile"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("Unsupported integrity hash algorithm");
expect(exitCode).toBe(0);
});
test("lockfile with garbage integrity string should fail", async () => {
using dir = tempDir("garbage-integrity", {
"package.json": JSON.stringify({
name: "test-garbage-integrity",
dependencies: {
"is-number": "7.0.0",
},
}),
"bun.lock": JSON.stringify(
{
lockfileVersion: 1,
configVersion: 1,
workspaces: {
"": {
name: "test-garbage-integrity",
dependencies: {
"is-number": "7.0.0",
},
},
},
packages: {
"is-number": ["is-number@7.0.0", "", {}, "not-a-real-hash"],
},
},
null,
2,
),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "install", "--frozen-lockfile"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("Unsupported integrity hash algorithm");
expect(exitCode).toBe(1);
});