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
+14 -3
View File
@@ -8,6 +8,8 @@ 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;
export default async function bunextRequestHandler({ req: initial_req, server, }) {
const is_dev = isDevelopment();
let req = initial_req.clone();
@@ -30,6 +32,11 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
}
}
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");
}
@@ -60,8 +67,12 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
return response;
}
catch (error) {
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 -1
View File
@@ -2,13 +2,14 @@ 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();
export default async function ({ req }) {
try {
const is_dev = isDevelopment();
const url = new URL(req.url);
const file_path = path.join(BUNEXT_PUBLIC_DIR, 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 });
}
return readFileResponse({
+7 -2
View File
@@ -2,13 +2,14 @@ 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();
export default async function ({ req }) {
try {
const is_dev = isDevelopment();
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 });
}
if (!existsSync(file_path)) {
@@ -17,7 +18,11 @@ export default async function ({ req }) {
});
}
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`, {
+19 -6
View File
@@ -1,5 +1,21 @@
function removeController(controller) {
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 }) {
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;
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
? global.BUNDLER_CTX_MAP?.[match.filePath]
@@ -20,16 +36,13 @@ export default async function ({ req }) {
}
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);
},
});
return new Response(stream, {
+5 -1
View File
@@ -2,13 +2,14 @@ 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();
export default async function ({ req }) {
try {
const is_dev = isDevelopment();
const url = new URL(req.url);
const file_path = path.join(PUBLIC_DIR, 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 });
}
return readFileResponse({ file_path });
@@ -33,6 +34,9 @@ export function readFileResponse({ file_path, cache }) {
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, {
headers,
});
+19 -3
View File
@@ -41,12 +41,13 @@ export default async function ({ req }) {
module = await import(import_path);
}
const config = module.config;
const maxBodyBytes = config?.max_request_body_mb
? config.max_request_body_mb * MBInBytes
: ServerDefaultRequestBodyLimitBytes;
const contentLength = req.headers.get("content-length");
if (contentLength) {
const size = parseInt(contentLength, 10);
if ((config?.max_request_body_mb &&
size > config.max_request_body_mb * MBInBytes) ||
size > ServerDefaultRequestBodyLimitBytes) {
if (size > maxBodyBytes) {
return Response.json({
success: false,
msg: "Request Body Too Large!",
@@ -58,6 +59,21 @@ export default async function ({ req }) {
});
}
}
else if (req.method !== "GET" && req.method !== "HEAD") {
const body = await req.arrayBuffer();
if (body.byteLength > maxBodyBytes) {
return Response.json({
success: false,
msg: "Request Body Too Large!",
}, {
status: 413,
headers: {
"Content-Type": "application/json",
},
});
}
routeParams.body = JSON.parse(new TextDecoder().decode(body) || "{}");
}
const target_module = (module["default"] ||
module["handler"]);
const res = await target_module?.({
+2 -2
View File
@@ -30,8 +30,8 @@ export default async function serverPostBuildFn(params) {
controller.controller.enqueue(reload_enqueue);
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
? await grabPageComponent({
+14 -9
View File
@@ -76,15 +76,20 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
console.error = () => { };
console.info = () => { };
console.debug = () => { };
const stream = await renderToReadableStream(final_component, {
onError(error) {
if (error.message.includes('unique "key" prop'))
return;
originalConsole.error(error);
},
});
const htmlBody = await new Response(stream).text();
Object.assign(console, originalConsole);
let htmlBody;
try {
const stream = await renderToReadableStream(final_component, {
onError(error) {
if (error.message.includes('unique "key" prop'))
return;
originalConsole.error(error);
},
});
htmlBody = await new Response(stream).text();
}
finally {
Object.assign(console, originalConsole);
}
html += htmlBody;
return html;
}
@@ -33,7 +33,7 @@ export default async function grabPageCombinedServerRes({ file_path, debug, url,
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 = final_page_server_path
? await import(`${final_page_server_path}?t=${now}`)
: undefined;
+1 -1
View File
@@ -72,7 +72,7 @@ export default async function grabPageComponent(params) {
}
}
if (req && !is_hydration) {
global.BUNDLER_CTX_MAP[file_path].req = req;
global.BUNDLER_CTX_MAP[file_path].req_url = req.url;
}
if (debug) {
log.info(`bundledMap:`, bundledMap);