diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index 6bec1cb4814e..132439599c07 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -799,6 +799,11 @@ impl AnyRoute { "To mount a directory, make sure the path ends in `/*`" ))); } + if strings::contains_char(relative_root, 0) { + return Err(global.throw_invalid_arguments(format_args!( + "\"dir\" must not contain null bytes" + ))); + } let style_js = argument.get(global, b"style")?; if style_js.is_none() { @@ -835,6 +840,30 @@ impl AnyRoute { FrameworkRouter::Style::from_js(style_js.unwrap(), global)?; // Style impls Drop; `?` drops it on the error path. + { + use bun_sys_jsc::ErrorJsc; + let mut buf = paths::path_buffer_pool::get(); + let Some(abs_root) = + paths::resolve_path::join_abs_string_buf_checked::( + paths::fs::FileSystem::instance().top_level_dir(), + &mut buf[..], + &[relative_root], + ) + else { + let err = sys::Error::from_code(sys::E::ENAMETOOLONG, sys::Tag::open) + .with_path(relative_root); + return Err(global.throw_value(err.to_js(global)?)); + }; + match sys::open_a( + abs_root, + sys::O::DIRECTORY | sys::O::CLOEXEC | sys::O::RDONLY, + 0, + ) { + Ok(fd) => drop(sys::File::from_fd(fd)), + Err(err) => return Err(global.throw_value(err.to_js(global)?)), + } + } + // trim the /* // NOTE: `FileSystemRouterType` fields are `Cow<'static,[u8]>`. // Rather diff --git a/test/js/bun/http/serve-directory-routes.test.ts b/test/js/bun/http/serve-directory-routes.test.ts index bd4cd5c85757..8e281b4eaef5 100644 --- a/test/js/bun/http/serve-directory-routes.test.ts +++ b/test/js/bun/http/serve-directory-routes.test.ts @@ -529,6 +529,48 @@ describe("Bun.serve() directory routes", () => { ).toThrow(expect.objectContaining({ code: "ENOENT" })); }); + it("throws if the directory of a { dir, style } mount does not exist", () => { + using dir = tempDir("serve-dir-style-enoent", {}); + expect(() => + serve({ + port: 0, + development: true, + routes: { "/*": { dir: join(String(dir), "does-not-exist"), style: "nextjs-pages" } }, + }), + ).toThrow(expect.objectContaining({ code: "ENOENT" })); + }); + + it("throws if the directory of a { dir, style } mount is a file", () => { + using dir = tempDir("serve-dir-style-enotdir", { pages: "not a directory" }); + expect(() => + serve({ + port: 0, + development: true, + routes: { "/*": { dir: join(String(dir), "pages"), style: "nextjs-pages" } }, + }), + ).toThrow(expect.objectContaining({ code: "ENOTDIR" })); + }); + + it("rejects a dir with an embedded null byte", () => { + using dir = tempDir("serve-dir-nul", { "public/x.txt": "x" }); + const withNul = join(String(dir), "public") + "\0ignored"; + expect(() => serve({ port: 0, routes: { "/*": { dir: withNul } } })).toThrow(/null bytes/); + expect(() => + serve({ port: 0, development: true, routes: { "/*": { dir: withNul, style: "nextjs-pages" } } }), + ).toThrow(/null bytes/); + }); + + it("throws if the directory of a { dir, style } mount is too long for a path", () => { + const dir = Buffer.alloc(100_000, "a").toString(); + expect(() => + serve({ + port: 0, + development: true, + routes: { "/*": { dir, style: "nextjs-pages" } }, + }), + ).toThrow(expect.objectContaining({ code: "ENAMETOOLONG" })); + }); + it("serves correctly with more unique paths than stat-cache slots", async () => { const N = 300; // > STAT_CACHE_SLOTS (256) const files: Record = {};