Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/install/TarballStream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,15 @@ impl TarballStream {
let rest: &[OSPathChar] = tokenize_rest_after_first(&pathname[..]);

let mut norm_buf = OSPathBuffer::uninit();
// `normalize_buf_t` writes into a fixed-size OSPathBuffer and assumes the
// caller provides enough space. Tarballs can contain arbitrarily long PAX
// paths, so reject entries that cannot fit including the trailing sentinel.
if rest.len() >= norm_buf.len() {
self.phase = Phase::WantData;
self.out_fd = None;
return Ok(());
}

let normalized =
resolve_path::normalize_buf_t::<OSPathChar, platform::Auto>(rest, &mut norm_buf[..]);
let norm_len = normalized.len();
Expand Down
17 changes: 17 additions & 0 deletions src/libarchive/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1777,6 +1777,13 @@ impl Archiver {
// at its original `.len()`; therefore `remaining[remaining.len()] == 0`.
let pathname: &[OSPathChar] = remaining;

// `normalize_buf_t` writes into a fixed-size OSPathBuffer and assumes the
// caller provides enough space. Tarballs can contain arbitrarily long PAX
// paths, so reject entries that cannot fit including the trailing sentinel.
if pathname.len() >= normalized_buf.len() {
continue 'loop_;
}

let normalized = bun_paths::resolve_path::normalize_buf_t::<
OSPathChar,
bun_paths::platform::Auto,
Expand All @@ -1790,6 +1797,16 @@ impl Archiver {
continue;
}

// `normalize_buf_t` collapses interior `..` but leaves a leading `..` on
// relative input. Reject those so extraction cannot escape `dir`.
if path.len() >= 2
&& path[0] == b'.' as OSPathChar
&& path[1] == b'.' as OSPathChar
&& (path.len() == 2 || path[2] == bun_paths::SEP as OSPathChar)
{
continue 'loop_;
}

// Skip entries whose normalized path is absolute on Windows.
// `openatWindows` ignores `dir_fd` for absolute inputs (drive
// letter or UNC), so without this guard a tar entry could
Expand Down
65 changes: 65 additions & 0 deletions test/cli/install/bun-install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
toHaveBins,
} from "harness";
import { join, resolve, sep } from "path";
import { gzipSync } from "zlib";
import {
createTestContext,
destroyTestContext,
Expand Down Expand Up @@ -66,7 +67,71 @@ async function withContext(
// Default context options for most tests
const defaultOpts = { linker: "hoisted" as const };

function tarHeader(name: string, size: number, type = "0") {
const header = Buffer.alloc(512, 0);
header.write(name, 0, Math.min(Buffer.byteLength(name), 100), "utf8");
header.write("0000644\0", 100, 8, "ascii");
header.write("0000000\0", 108, 8, "ascii");
header.write("0000000\0", 116, 8, "ascii");
header.write(size.toString(8).padStart(11, "0") + "\0", 124, 12, "ascii");
header.write("00000000000\0", 136, 12, "ascii");
header.fill(" ", 148, 156);
header.write(type, 156, 1, "ascii");
header.write("ustar\0", 257, 6, "ascii");
header.write("00", 263, 2, "ascii");

let checksum = 0;
for (const byte of header) checksum += byte;
header.write(checksum.toString(8).padStart(6, "0") + "\0 ", 148, 8, "ascii");
return header;
}

function tarEntry(name: string, body: string | Buffer, type = "0") {
const contents = Buffer.isBuffer(body) ? body : Buffer.from(body);
const padding = Buffer.alloc((512 - (contents.length % 512)) % 512);
return Buffer.concat([tarHeader(name, contents.length, type), contents, padding]);
}

function paxEntry(path: string) {
const body = ` path=${path}\n`;
let digits = 1;
while (true) {
const length = digits + Buffer.byteLength(body);
const nextDigits = String(length).length;
if (nextDigits === digits) return Buffer.from(`${length}${body}`);
digits = nextDigits;
}
}

describe.concurrent("bun-install", () => {
it("does not crash extracting npm tarballs with overlong PAX paths", async () => {
const package_dir = tempDir("bun-install-long-pax-path", {
"package.json": JSON.stringify({ dependencies: { x: "file:./pkg.tgz" } }),
});

const tarball = gzipSync(
Buffer.concat([
tarEntry("PaxHeader", paxEntry(`package/${Buffer.alloc(5000, "a").toString()}`), "x"),
tarEntry("package/short", "x"),
tarEntry("package/package.json", JSON.stringify({ name: "x", version: "1.0.0" })),
Buffer.alloc(1024),
]),
);
await writeFile(join(package_dir, "pkg.tgz"), tarball);

const { stderr, exited } = spawn({
cmd: [bunExe(), "install", "--no-progress"],
cwd: package_dir,
env,
stdout: "pipe",
stderr: "pipe",
});

const err = await new Response(stderr).text();
expect(err).toContain("Saved lockfile");
Comment thread
pc-style marked this conversation as resolved.
expect(await exited).toBe(0);
});

for (let input of ["abcdef", "65537", "-1"]) {
it(`bun install --network-concurrency=${input} fails`, async () => {
await withContext(defaultOpts, async ctx => {
Expand Down