mirror of
https://github.com/oven-sh/bun
synced 2026-02-18 14:51:52 +00:00
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>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = .{},
|
||||
|
||||
126
test/cli/install/unsupported-integrity-hash.test.ts
Normal file
126
test/cli/install/unsupported-integrity-hash.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user