diff --git a/src/runtime/webcore/Blob.rs b/src/runtime/webcore/Blob.rs index 29c3e68f94bd..a42512ff2741 100644 --- a/src/runtime/webcore/Blob.rs +++ b/src/runtime/webcore/Blob.rs @@ -5541,12 +5541,18 @@ pub fn construct_bun_file( }; if let PathOrFileDescriptor::Path(ref p) = path { - if p.slice().starts_with(b"s3://") { + let path_slice = p.slice(); + if path_slice.starts_with(b"s3://") { // PORT NOTE (layering): `webcore::node_types::PathLike` re-exports // `crate::node::types::PathLike`, so no conversion is needed — // clone the path (Zig consumed it; the Rust `path` drops at scope exit). return S3File::construct_internal_js(global_object, p.clone(), options); } + if is_http_url_path(path_slice) { + return Err(global_object.throw_invalid_arguments(format_args!( + "Bun.file() does not support HTTP URLs. Use fetch() instead." + ))); + } } // PORT NOTE: Zig `defer path.deinitAndUnprotect()` — sync path took no // `protect()`, so `Drop for PathOrFileDescriptor` suffices. @@ -5593,6 +5599,14 @@ pub fn construct_bun_file( Ok(unsafe { BlobExt::to_js(&*ptr, global_object) }) } +fn is_http_url_path(path: &[u8]) -> bool { + path.get(.."http://".len()) + .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"http://")) + || path + .get(.."https://".len()) + .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"https://")) +} + // `find_or_create_file_from_path`: canonical impl lives later in this file // (runtime `check_s3: bool` form). Const-generic duplicate removed here. diff --git a/src/runtime/webcore/Blob.zig b/src/runtime/webcore/Blob.zig index 6cf5c0475545..5d5df50df3f2 100644 --- a/src/runtime/webcore/Blob.zig +++ b/src/runtime/webcore/Blob.zig @@ -2068,9 +2068,13 @@ pub fn constructBunFile( const options = if (arguments.len >= 2) arguments[1] else null; if (path == .path) { - if (strings.hasPrefixComptime(path.path.slice(), "s3://")) { + const path_slice = path.path.slice(); + if (strings.hasPrefixComptime(path_slice, "s3://")) { return try S3File.constructInternalJS(globalObject, path.path, options); } + if (isHTTPURLPath(path_slice)) { + return globalObject.throwInvalidArguments("Bun.file() does not support HTTP URLs. Use fetch() instead.", .{}); + } } defer path.deinitAndUnprotect(); @@ -2109,6 +2113,11 @@ pub fn constructBunFile( return ptr.toJS(globalObject); } +fn isHTTPURLPath(path: []const u8) bool { + return (path.len >= "http://".len and strings.eqlCaseInsensitiveASCII(path[0.."http://".len], "http://", true)) or + (path.len >= "https://".len and strings.eqlCaseInsensitiveASCII(path[0.."https://".len], "https://", true)); +} + pub fn findOrCreateFileFromPath(path_or_fd: *jsc.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject, comptime check_s3: bool) Blob { var vm = globalThis.bunVM(); const allocator = bun.default_allocator; diff --git a/test/js/bun/util/bun-file.test.ts b/test/js/bun/util/bun-file.test.ts index f54f12e48863..f2d10d4da717 100644 --- a/test/js/bun/util/bun-file.test.ts +++ b/test/js/bun/util/bun-file.test.ts @@ -115,6 +115,13 @@ test("Bun.file().arrayBuffer() errors include async stack frames", async () => { expect(caught.stack).toContain("at async caller"); }); +test("Bun.file() rejects remote URL strings with a helpful error", () => { + const message = "Bun.file() does not support HTTP URLs. Use fetch() instead."; + + expect(() => Bun.file("https://example.com/image.png")).toThrow(message); + expect(() => Bun.file("http://example.com/image.png")).toThrow(message); +}); + test("Bun.file().json() with UTF-8 BOM does not free an interior pointer", async () => { // When a file starts with EF BB BF, the BOM is stripped before parsing and // the temporary read buffer is freed. Previously the *post-strip* slice was