Security fixes pass #2

This commit is contained in:
2026-04-19 16:00:59 +01:00
parent 3b26292124
commit b702e26bf6
40 changed files with 305 additions and 93 deletions
+16 -3
View File
@@ -8,6 +8,10 @@ import handleBunextPublicAssets from "./handle-bunext-public-assets";
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
import { AppData } from "../../data/app-data";
import fullRebuild from "./full-rebuild";
const HMR_RETRY_COOLDOWN_MS = 5000;
let lastHmrRetryTime = 0;
type Params = {
req: Request;
server: Bun.Server<any>;
@@ -45,6 +49,11 @@ export default async function bunextRequestHandler({
}
if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) {
const now = Date.now();
if (now - lastHmrRetryTime < HMR_RETRY_COOLDOWN_MS) {
return new Response("Too Many Requests", { status: 429 });
}
lastHmrRetryTime = now;
await fullRebuild({ msg: `HMR Retry Rebuild ...` });
return new Response("Modules Rebuilt");
}
@@ -76,8 +85,12 @@ export default async function bunextRequestHandler({
return response;
} catch (error: any) {
return new Response(`Server Error: ${error.message}`, {
status: 500,
});
if (is_dev) {
return new Response(`Server Error: ${error.message}`, {
status: 500,
});
}
console.error(`Server Error: ${error.message}`, error);
return new Response("Internal Server Error", { status: 500 });
}
}
@@ -2,6 +2,7 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path";
import isDevelopment from "../../utils/is-development";
import { readFileResponse } from "./handle-public";
import isSafePath from "../../utils/is-safe-path";
const { BUNEXT_PUBLIC_DIR } = grabDirNames();
@@ -19,7 +20,7 @@ export default async function ({ req }: Params): Promise<Response> {
url.pathname.replace(/\/\.bunext\/public\//, ""),
);
if (!file_path.startsWith(BUNEXT_PUBLIC_DIR + path.sep)) {
if (!isSafePath({ filePath: file_path, allowedDir: BUNEXT_PUBLIC_DIR })) {
return new Response("Forbidden", { status: 403 });
}
+9 -2
View File
@@ -2,6 +2,7 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path";
import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs";
import isSafePath from "../../utils/is-safe-path";
const { PUBLIC_DIR } = grabDirNames();
@@ -15,7 +16,7 @@ export default async function ({ req }: Params): Promise<Response> {
const url = new URL(req.url);
const file_path = path.join(PUBLIC_DIR, url.pathname);
if (!file_path.startsWith(PUBLIC_DIR + path.sep)) {
if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) {
return new Response("Forbidden", { status: 403 });
}
@@ -26,7 +27,13 @@ export default async function ({ req }: Params): Promise<Response> {
}
const file = Bun.file(file_path);
return new Response(file);
const headers = new Headers();
if (!is_dev) {
headers.set("Cache-Control", "public, max-age=3600");
}
return new Response(file, { headers });
} catch (error) {
return new Response(`File Not Found`, {
status: 404,
+23 -11
View File
@@ -2,8 +2,28 @@ type Params = {
req: Request;
};
function removeController(controller: ReadableStreamDefaultController<string>) {
const idx = global.HMR_CONTROLLERS.findIndex(
(c) => c.controller == controller,
);
if (typeof idx == "number" && idx >= 0) {
global.HMR_CONTROLLERS.splice(idx, 1);
}
}
export default async function ({ req }: Params): Promise<Response> {
const referer_url = new URL(req.headers.get("referer") || "");
const referer = req.headers.get("referer");
if (!referer) {
return new Response("Missing Referer Header", { status: 400 });
}
let referer_url: URL;
try {
referer_url = new URL(referer);
} catch {
return new Response("Invalid Referer Header", { status: 400 });
}
const match = global.ROUTER.match(referer_url.pathname);
const target_map = match?.filePath
@@ -25,21 +45,13 @@ export default async function ({ req }: Params): Promise<Response> {
c.enqueue(": keep-alive\n\n");
} catch {
clearInterval(heartbeat);
removeController(controller);
}
}, 5000);
},
cancel() {
clearInterval(heartbeat);
const targetControllerIndex = global.HMR_CONTROLLERS.findIndex(
(c) => c.controller == controller,
);
if (
typeof targetControllerIndex == "number" &&
targetControllerIndex >= 0
) {
global.HMR_CONTROLLERS.splice(targetControllerIndex, 1);
}
removeController(controller);
},
});
+4 -1
View File
@@ -2,6 +2,7 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path";
import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs";
import isSafePath from "../../utils/is-safe-path";
const { PUBLIC_DIR } = grabDirNames();
@@ -19,7 +20,7 @@ export default async function ({ req }: Params): Promise<Response> {
url.pathname.replace(/^\/public/, ""),
);
if (!file_path.startsWith(PUBLIC_DIR + path.sep)) {
if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) {
return new Response("Forbidden", { status: 403 });
}
@@ -53,6 +54,8 @@ export function readFileResponse({ file_path, cache }: FileResponse) {
headers.set("Cache-Control", "public, max-age=31536000, immutable");
} else if (cache?.duration) {
headers.set("Cache-Control", `public, max-age=${cache.duration}`);
} else if (!isDevelopment()) {
headers.set("Cache-Control", "public, max-age=3600");
}
return new Response(file, {
+2 -2
View File
@@ -45,8 +45,8 @@ export default async function serverPostBuildFn(params?: Params) {
continue;
}
const mock_req = target_artifact.req
? target_artifact.req.clone()
const mock_req = target_artifact.req_url
? new Request(target_artifact.req_url)
: new Request(controller.page_url);
const page_component = global.IS_SERVER_COMPONENT
@@ -178,16 +178,19 @@ export default async function genWebHTML({
console.info = () => {};
console.debug = () => {};
const stream = await renderToReadableStream(final_component, {
onError(error: any) {
if (error.message.includes('unique "key" prop')) return;
originalConsole.error(error);
},
});
let htmlBody: string;
try {
const stream = await renderToReadableStream(final_component, {
onError(error: any) {
if (error.message.includes('unique "key" prop')) return;
originalConsole.error(error);
},
});
const htmlBody = await new Response(stream).text();
Object.assign(console, originalConsole);
htmlBody = await new Response(stream).text();
} finally {
Object.assign(console, originalConsole);
}
html += htmlBody;
@@ -63,7 +63,7 @@ export default async function grabPageCombinedServerRes({
const page_server_ctx = global.SSR_BUNDLER_CTX_MAP[server_file_path || ""];
const final_page_server_path = page_server_ctx?.local_path
? path.join(ROOT_DIR, page_server_ctx.path)
: root_server_file_path;
: server_file_path;
const server_module: BunextPageServerModule = final_page_server_path
? await import(`${final_page_server_path}?t=${now}`)
@@ -117,7 +117,7 @@ export default async function grabPageComponent(
}
if (req && !is_hydration) {
global.BUNDLER_CTX_MAP[file_path].req = req;
global.BUNDLER_CTX_MAP[file_path].req_url = req.url;
}
if (debug) {