Compare commits

...

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
7d19568644 [autofix.ci] apply automated fixes 2026-02-26 21:00:49 +00:00
Claude Bot
392108a932 fix(formdata): preserve binary data with null bytes in multipart parsing
`Field.value` used `bun.Semver.String` which treats inline data (<=8 bytes)
as null-terminated strings, truncating binary file content at the first 0x00
byte. Replace with a raw `[]const u8` slice into the input buffer.

Closes #27478

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-26 20:58:36 +00:00
3 changed files with 136 additions and 13 deletions

View File

@@ -228,16 +228,16 @@ To build for macOS x64:
The order of the `--target` flag does not matter, as long as they're delimited by a `-`.
| --target | Operating System | Architecture | Modern | Baseline | Libc |
| --------------------- | ---------------- | ------------ | ------ | -------- | ----- |
| bun-linux-x64 | Linux | x64 | ✅ | ✅ | glibc |
| bun-linux-arm64 | Linux | arm64 | ✅ | N/A | glibc |
| bun-windows-x64 | Windows | x64 | ✅ | ✅ | - |
| bun-windows-arm64 | Windows | arm64 | ✅ | N/A | - |
| bun-darwin-x64 | macOS | x64 | ✅ | ✅ | - |
| bun-darwin-arm64 | macOS | arm64 | ✅ | N/A | - |
| bun-linux-x64-musl | Linux | x64 | ✅ | ✅ | musl |
| bun-linux-arm64-musl | Linux | arm64 | ✅ | N/A | musl |
| --target | Operating System | Architecture | Modern | Baseline | Libc |
| -------------------- | ---------------- | ------------ | ------ | -------- | ----- |
| bun-linux-x64 | Linux | x64 | ✅ | ✅ | glibc |
| bun-linux-arm64 | Linux | arm64 | ✅ | N/A | glibc |
| bun-windows-x64 | Windows | x64 | ✅ | ✅ | - |
| bun-windows-arm64 | Windows | arm64 | ✅ | N/A | - |
| bun-darwin-x64 | macOS | x64 | ✅ | ✅ | - |
| bun-darwin-arm64 | macOS | arm64 | ✅ | N/A | - |
| bun-linux-x64-musl | Linux | x64 | ✅ | ✅ | musl |
| bun-linux-arm64-musl | Linux | arm64 | ✅ | N/A | musl |
<Warning>
On x64 platforms, Bun uses SIMD optimizations which require a modern CPU supporting AVX2 instructions. The `-baseline`

View File

@@ -976,7 +976,10 @@ pub const FormData = struct {
}
pub const Field = struct {
value: bun.Semver.String = .{},
/// Raw slice into the input buffer. Not using `bun.Semver.String` because
/// file bodies are binary data that can contain null bytes, which
/// Semver.String's inline storage treats as terminators.
value: []const u8 = "",
filename: bun.Semver.String = .{},
content_type: bun.Semver.String = .{},
is_file: bool = false,
@@ -1088,7 +1091,7 @@ pub const FormData = struct {
form: *jsc.DOMFormData,
pub fn onEntry(wrap: *@This(), name: bun.Semver.String, field: Field, buf: []const u8) void {
const value_str = field.value.slice(buf);
const value_str = field.value;
var key = jsc.ZigString.initUTF8(name.slice(buf));
if (field.is_file) {
@@ -1278,7 +1281,7 @@ pub const FormData = struct {
if (strings.endsWithComptime(body, "\r\n")) {
body = body[0 .. body.len - 2];
}
field.value = subslicer.sub(body).value();
field.value = body;
field.filename = filename orelse .{};
field.is_file = is_file;

View File

@@ -0,0 +1,120 @@
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/27478
// Request.formData() truncates small binary files at first null byte
test("multipart formdata preserves null bytes in small binary files", async () => {
const boundary = "----bun-null-byte-boundary";
const source = Buffer.from([0x1f, 0x8b, 0x08, 0x00]);
const payload = Buffer.concat([
Buffer.from(
`--${boundary}\r\n` +
`Content-Disposition: form-data; name="file"; filename="test.bin"\r\n` +
`Content-Type: application/octet-stream\r\n\r\n`,
"utf8",
),
source,
Buffer.from(`\r\n--${boundary}--\r\n`, "utf8"),
]);
const request = new Request("http://localhost/", {
method: "POST",
headers: { "content-type": `multipart/form-data; boundary=${boundary}` },
body: payload,
});
const form = await request.formData();
const file = form.get("file");
expect(file).toBeInstanceOf(File);
const parsed = new Uint8Array(await (file as File).arrayBuffer());
expect(Array.from(parsed)).toEqual(Array.from(source));
expect(parsed.byteLength).toBe(source.byteLength);
});
test("multipart formdata preserves files that are all null bytes", async () => {
const boundary = "----bun-test-boundary";
const source = Buffer.from([0x00, 0x00, 0x00, 0x00]);
const payload = Buffer.concat([
Buffer.from(
`--${boundary}\r\n` +
`Content-Disposition: form-data; name="file"; filename="zeros.bin"\r\n` +
`Content-Type: application/octet-stream\r\n\r\n`,
"utf8",
),
source,
Buffer.from(`\r\n--${boundary}--\r\n`, "utf8"),
]);
const request = new Request("http://localhost/", {
method: "POST",
headers: { "content-type": `multipart/form-data; boundary=${boundary}` },
body: payload,
});
const form = await request.formData();
const file = form.get("file");
expect(file).toBeInstanceOf(File);
const parsed = new Uint8Array(await (file as File).arrayBuffer());
expect(Array.from(parsed)).toEqual([0x00, 0x00, 0x00, 0x00]);
expect(parsed.byteLength).toBe(4);
});
test("multipart formdata preserves single null byte file", async () => {
const boundary = "----bun-test-boundary";
const source = Buffer.from([0x00]);
const payload = Buffer.concat([
Buffer.from(
`--${boundary}\r\n` +
`Content-Disposition: form-data; name="file"; filename="null.bin"\r\n` +
`Content-Type: application/octet-stream\r\n\r\n`,
"utf8",
),
source,
Buffer.from(`\r\n--${boundary}--\r\n`, "utf8"),
]);
const request = new Request("http://localhost/", {
method: "POST",
headers: { "content-type": `multipart/form-data; boundary=${boundary}` },
body: payload,
});
const form = await request.formData();
const file = form.get("file");
expect(file).toBeInstanceOf(File);
const parsed = new Uint8Array(await (file as File).arrayBuffer());
expect(Array.from(parsed)).toEqual([0x00]);
expect(parsed.byteLength).toBe(1);
});
test("multipart formdata preserves 8-byte binary with embedded nulls", async () => {
const boundary = "----bun-test-boundary";
// Exactly 8 bytes (max inline length of Semver.String) with nulls interspersed
const source = Buffer.from([0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00]);
const payload = Buffer.concat([
Buffer.from(
`--${boundary}\r\n` +
`Content-Disposition: form-data; name="file"; filename="mixed.bin"\r\n` +
`Content-Type: application/octet-stream\r\n\r\n`,
"utf8",
),
source,
Buffer.from(`\r\n--${boundary}--\r\n`, "utf8"),
]);
const request = new Request("http://localhost/", {
method: "POST",
headers: { "content-type": `multipart/form-data; boundary=${boundary}` },
body: payload,
});
const form = await request.formData();
const file = form.get("file");
expect(file).toBeInstanceOf(File);
const parsed = new Uint8Array(await (file as File).arrayBuffer());
expect(Array.from(parsed)).toEqual([0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00]);
expect(parsed.byteLength).toBe(8);
});