FormData: send only basename, not full path, as Bun.file() multipart filename

RO
2mo ago
+75-14

Repro

import { mkdirSync, writeFileSync } from "node:fs";
mkdirSync("/tmp/secret-dir", { recursive: true });
writeFileSync("/tmp/secret-dir/report.pdf", "%PDF");
const fd = new FormData();
fd.append("upload", Bun.file("/tmp/secret-dir/report.pdf"));
console.log((await new Request("http://x/", { method: "POST", body: fd }).text())
  .split("\r\n").find(l => l.startsWith("Content-Disposition")));

Before:

Content-Disposition: form-data; name="upload"; filename="/tmp/secret-dir/report.pdf"

After (and in browsers):

Content-Disposition: form-data; name="upload"; filename="report.pdf"

Cause

FormData.append/set default the entry filename from Blob__getFileNameString when no explicit filename is given (JSDOMFormData.cpp:311/474). For a Bun.file(path) blob that returned the full host path, which was emitted verbatim in the multipart Content-Disposition header. The documented upload shape (fd.append("f", Bun.file(p)); fetch(url, { method: "POST", body: fd })) therefore disclosed the client's directory layout, usernames, and temp paths to the server.

A browser File reports only the basename (File API: "the name of the file without path information"), and Node's fs.openAsBlob part serializes filename="blob"; neither leaks a path.

Fix

Blob__getFileNameString returns an assigned name as given, and otherwise basename(store_path) (a Bun.file() path or S3 key), computed on the raw path bytes. Its only callers are the FormData.append/set default-filename paths.

For that to be a clean rule, get_name_string() no longer caches the derived path into blob.name. Before, the first .name read wrote the full path into name, which made an assigned name and a derived one indistinguishable. The JS name getter is already cached by JSC on the wrapper (cache: true), so nothing is recomputed on repeated reads from JS.

Unchanged:

  • Bun.file(path).name still returns the full path.
  • An explicit fd.append(name, blob, filename) argument is still used verbatim.
  • An assigned name is used verbatim: new File([..], "reports/q3.pdf"), new File([Bun.file(p)], "reports/q3.pdf"), bunFile.name = "renamed.pdf".
  • A File read back out of a FormData (fd.get()) and appended again still serializes the basename.

Verification

bun bd test test/js/web/html/FormData.test.ts

150 pass / 0 fail. The new test fails on the current release (emits the full path) and passes with this change. Also green: FormData-multipart-serialization, form-data-set-append, fetch-file-upload, FormData-file-error-leak, structured-clone-blob-file, structured-clone-fastpath, fetch/blob, fetch/body, util/inspect.


[review] gate passed · iteration 8 · 2 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/html/FormData.test.ts
bun test v1.4.1 (adc354d99)

test/js/web/html/FormData.test.ts:
(pass) FormData > should be able to append a string [3.22ms]
(pass) FormData > should be able to append a Blob [8.24ms]
(pass) FormData > should be able to set a Blob [5.72ms]
(pass) FormData > should be able to set a string [2.64ms]
(pass) FormData > should get filename from file [3.83ms]
(pass) FormData > should use the correct filenames [4.82ms]
(pass) FormData > should parse multipart/form-data (simple) with Response [12.99ms]
(pass) FormData > should roundtrip multipart/form-data (simple) with Response [28.39ms]
(pass) FormData > should parse multipart/form-data (simple) with Request [2.32ms]
(pass) FormData > should roundtrip multipart/form-data (simple) with Request [5.00ms]
(pass) FormData > should parse multipart/form-data (simple with trailing CRLF) with Response [1.55ms]
(pass) FormData > should roundtrip multipart/form-data (simple with trailing CRLF) with Response [3.71ms]
(pass) FormData > should parse multipart/form-data (si
... (truncated)

release without fix: all passed
bun test v1.4.1-canary.1 (aacd9512f)

test/js/web/html/FormData.test.ts:
(pass) FormData > should be able to append a string [0.06ms]
(pass) FormData > should be able to append a Blob [0.12ms]
(pass) FormData > should be able to set a Blob [0.06ms]
(pass) FormData > should be able to set a string [0.03ms]
(pass) FormData > should get filename from file [0.04ms]
(pass) FormData > should use the correct filenames [0.05ms]
(pass) FormData > should parse multipart/form-data (simple) with Response [0.27ms]
(pass) FormData > should roundtrip multipart/form-data (simple) with Response [0.55ms]
(pass) FormData > should parse multipart/form-data (simple) with Request [0.03ms]
(pass) FormData > should roundtrip multipart/form-data (simple) with Request [0.03ms]
(pass) FormData > should parse multipart/form-data (simple with trailing CRLF) with Response [0.02ms]
(pass) FormData > should roundtrip multipart/form-data (simple with trailing CRLF) with Response [0.02ms]
(pass) FormData > should parse multipart/form-data (simple with trailing CRLF) with Request [0.01ms]
(pass) FormData > should roundtrip multipart/form-data (simple with trailing CRLF) with Request [0.01ms]
(pass) F
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/html/FormData.test.ts
bun test v1.4.1 (adc354d99)

test/js/web/html/FormData.test.ts:
(pass) FormData > should be able to append a string [3.30ms]
(pass) FormData > should be able to append a Blob [7.96ms]
(pass) FormData > should be able to set a Blob [5.63ms]
(pass) FormData > should be able to set a string [2.63ms]
(pass) FormData > should get filename from file [3.31ms]
(pass) FormData > should use the correct filenames [4.75ms]
(pass) FormData > should parse multipart/form-data (simple) with Response [13.37ms]
(pass) FormData > should roundtrip multipart/form-data (simple) with Response [28.73ms]
(pass) FormData > should parse multipart/form-data (simple) with Request [2.33ms]
(pass) FormData > should roundtrip multipart/form-data (simple) with Request [5.53ms]
(pass) FormData > should parse multipart/form-data (simple with trailing CRLF) with Response [1.61ms]
(pass) FormData > should roundtrip multipart/form-data (simple with trailing CRLF) with Response [3.46ms]
(pass) FormData > should parse multipart/form-data (si
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 582ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen generated_host_exports.rs
generated_host_exports.rs: 116 exports (host=5, lazy=10, generic=101, rust=0); 242 extern-C blocks audited
[1/6] cargo bun_runtime → libbun_runtime.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

   Compiling bun_core v0.0.0 (/workspace/bun/src/bun_core)
   Compiling bun_errno v0.0.0 (/workspace/bun/src/errno)
   Compiling bun_ptr v0.0.0 (/workspace/bun/src/ptr)
   Compiling bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
   Compiling bun_safety v0.0.0 (/workspace/bun/src/safety)
   Compiling bun_base64 v0.0.0 (/workspace/bun/src/base64)
   Compiling bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
   Compiling bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
   Compiling bun_zstd v0.0.0 (/workspace/bun/src/zstd)
   Compiling bun_pic
... (truncated)
diff hotspot
src/runtime/webcore/Blob.rs       | 29 ++++++++++---------
 test/js/web/html/FormData.test.ts | 60 ++++++++++++++++++++++++++++++++++++++-
 2 files changed, 75 insertions(+), 14 deletions(-)

gate history · 5 passed · 2 rejected · iteration 8

evidence per changed file
file                               reads  edits  tests
src/runtime/webcore/Blob.rs           14     11      0
test/js/web/html/FormData.test.ts      2      9      0