Fix security vulnerabilities and request handling bugs

- Move page cache out of the publicly served .bunext/public dir and hash cache keys
- Skip caching credentialed requests, non-2xx and Set-Cookie responses; cap entries
- Enforce API body size limit while reading; keep req body readable for handlers
- Hide error messages from clients in production
- Escape inline script JSON; strip cookies from HMR payloads
- Only trust X-Forwarded-Proto with trust_proxy config
- Fix console race in SSR, dotted page paths, middleware rewrites,
  excluded path status, encoded static file names, symlinked dirs
- Upgrade vulnerable deps, remove micromatch, pin bun versions, delete dead code

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 15:48:08 +01:00
co-authored by Claude Opus 5.5
parent 4238d9a4ea
commit 74103a19c9
37 changed files with 708 additions and 573 deletions
@@ -1,5 +1,5 @@
import grabContants from "../../../utils/grab-constants";
import EJSON from "../../../utils/ejson";
import serializeForScript from "../../../utils/serialize-for-script";
import type { LivePageDistGenParams } from "../../../types";
import isDevelopment from "../../../utils/is-development";
import grabWebPageHydrationScript from "./grab-web-page-hydration-script";
@@ -37,10 +37,11 @@ export default async function genWebHTML({
throw new Error(`Main Component not found!`);
}
const serializedProps = (EJSON.stringify(pageProps || {}) || "{}").replace(
/<\//g,
"<\\/",
);
let serializedProps = "{}";
try {
serializedProps = serializeForScript(pageProps || {});
} catch (error) {}
const page_hydration_script = await grabWebPageHydrationScript();
const root_meta = root_module?.meta
@@ -106,7 +107,7 @@ export default async function genWebHTML({
<script
dangerouslySetInnerHTML={{
__html: `window.${ClientWindowPagePropsName} = ${serializedProps};\nwindow.process = ${JSON.stringify(client_process)}`,
__html: `window.${ClientWindowPagePropsName} = ${serializedProps};\nwindow.process = ${serializeForScript(client_process)}`,
}}
data-bunext-head
/>
@@ -121,7 +122,7 @@ export default async function genWebHTML({
<script
type="importmap"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
__html: serializeForScript(
global.BUNEXT_REACT_IMPORTS_MAP,
),
}}
@@ -170,19 +171,7 @@ export default async function genWebHTML({
// const htmlBody = await new Response(stream).text();
const originalConsole = {
log: console.log,
warn: console.warn,
error: console.error,
info: console.info,
debug: console.debug,
};
console.log = () => {};
console.warn = () => {};
console.error = () => {};
console.info = () => {};
console.debug = () => {};
silenceConsole();
let htmlBody: string;
let render_error: any;
@@ -198,13 +187,13 @@ export default async function genWebHTML({
render_error = error;
}
originalConsole.error(error);
original_console.error(error);
},
});
htmlBody = await new Response(stream).text();
} finally {
Object.assign(console, originalConsole);
restoreConsole();
}
// A component crash during SSR must surface as a proper error (handled
@@ -218,3 +207,41 @@ export default async function genWebHTML({
return html;
}
let original_console: Pick<
Console,
"log" | "warn" | "error" | "info" | "debug"
> = console;
let silenced_renders = 0;
/**
* Renders can overlap (each one awaits the stream), so the console is
* swapped only by the first active render and restored by the last one.
* Otherwise a render could save another render's no-ops as the "original"
* console and silence the server for good.
*/
function silenceConsole() {
if (silenced_renders++ > 0) return;
original_console = {
log: console.log,
warn: console.warn,
error: console.error,
info: console.info,
debug: console.debug,
};
console.log = () => {};
console.warn = () => {};
console.error = () => {};
console.info = () => {};
console.debug = () => {};
}
function restoreConsole() {
if (--silenced_renders > 0) return;
silenced_renders = 0;
Object.assign(console, original_console);
}