Security fixes pass #2
This commit is contained in:
@@ -3,6 +3,10 @@ import path from "path";
|
||||
import type { BunSpawnOptions } from "../../types";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
|
||||
let retries = 0;
|
||||
let timeout: any;
|
||||
const MAX_RETRIES = 5;
|
||||
|
||||
export default function () {
|
||||
return new Command("start")
|
||||
.description("Start production server")
|
||||
@@ -12,6 +16,13 @@ export default function () {
|
||||
}
|
||||
|
||||
async function start() {
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (retries >= MAX_RETRIES) {
|
||||
console.error(`Production server crashed ${MAX_RETRIES} times. Exiting.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dev_spawn_file = path.resolve(__dirname, "prod-spawn.ts");
|
||||
|
||||
const spawn_options: BunSpawnOptions = {
|
||||
@@ -28,6 +39,12 @@ async function start() {
|
||||
|
||||
let dev_process = Bun.spawn(spawn_options);
|
||||
|
||||
retries++;
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
retries = 0;
|
||||
}, 10000);
|
||||
|
||||
const exited = await dev_process.exited;
|
||||
|
||||
if (exited) {
|
||||
|
||||
@@ -9,7 +9,7 @@ export default async function buildOnstartErrorHandler(params?: Params) {
|
||||
global.RECOMPILING = false;
|
||||
global.IS_SERVER_COMPONENT = false;
|
||||
|
||||
Promise.all([
|
||||
await Promise.all([
|
||||
global.SSR_BUNDLER_CTX?.dispose(),
|
||||
global.BUNDLER_CTX?.dispose(),
|
||||
]);
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
PageFiles,
|
||||
} from "../types";
|
||||
import type { FileSystemRouter, Server } from "bun";
|
||||
import grabDirNames from "../utils/grab-dir-names";
|
||||
import grabDirNames, { type DirNames } from "../utils/grab-dir-names";
|
||||
import { type FSWatcher } from "fs";
|
||||
import init from "./init";
|
||||
import isDevelopment from "../utils/is-development";
|
||||
@@ -43,7 +43,7 @@ declare global {
|
||||
var BUNDLER_CTX: BuildContext | undefined;
|
||||
var SSR_BUNDLER_CTX: BuildContext | undefined;
|
||||
// var API_ROUTES_BUNDLER_CTX: BuildContext | undefined;
|
||||
var DIR_NAMES: ReturnType<typeof grabDirNames>;
|
||||
var DIR_NAMES: DirNames;
|
||||
var REACT_IMPORTS_MAP: { imports: Record<string, string> };
|
||||
var REACT_DOM_SERVER: any;
|
||||
var REACT_DOM_MODULE_CACHE: Map<string, { main: any; css: string }>;
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ export default async function trimAllCache() {
|
||||
const trim_key = await trimCacheKey({
|
||||
key: cache_key,
|
||||
});
|
||||
|
||||
if (trim_key.success) {
|
||||
cached_items.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+1
-1
@@ -344,7 +344,7 @@ export type BundlerCTXMap = {
|
||||
url_path: string;
|
||||
file_name: string;
|
||||
css_path?: string;
|
||||
req?: Request;
|
||||
req_url?: string;
|
||||
};
|
||||
|
||||
export type GlobalHMRControllerObject = {
|
||||
|
||||
@@ -1,6 +1,38 @@
|
||||
import path from "path";
|
||||
|
||||
export default function grabDirNames() {
|
||||
export type DirNames = {
|
||||
ROOT_DIR: string;
|
||||
SRC_DIR: string;
|
||||
PAGES_DIR: string;
|
||||
API_DIR: string;
|
||||
PUBLIC_DIR: string;
|
||||
HYDRATION_DST_DIR: string;
|
||||
BUNX_CWD_DIR: string;
|
||||
BUNX_ROOT_DIR: string;
|
||||
CONFIG_FILE: string;
|
||||
BUNX_TMP_DIR: string;
|
||||
BUNX_HYDRATION_SRC_DIR: string;
|
||||
BUNX_ROOT_SRC_DIR: string;
|
||||
BUNX_ROOT_PRESETS_DIR: string;
|
||||
BUNX_ROOT_500_PRESET_COMPONENT: string;
|
||||
BUNX_ROOT_500_FILE_NAME: string;
|
||||
BUNX_ROOT_404_PRESET_COMPONENT: string;
|
||||
BUNX_ROOT_404_FILE_NAME: string;
|
||||
HYDRATION_DST_DIR_MAP_JSON_FILE: string;
|
||||
BUNEXT_CACHE_DIR: string;
|
||||
BUNX_CWD_MODULE_CACHE_DIR: string;
|
||||
BUNX_CWD_PAGES_REWRITE_DIR: string;
|
||||
HYDRATION_DST_DIR_MAP_JSON_FILE_NAME: string;
|
||||
BUNEXT_VENDOR_DIR: string;
|
||||
BUNEXT_PUBLIC_DIR: string;
|
||||
BUNX_BUNDLER_ERROR_EXIT_FILE: string;
|
||||
BUNX_ERROR_LOGS_DIR: string;
|
||||
BUNX_LOGS_DIR: string;
|
||||
};
|
||||
|
||||
export default function grabDirNames(): DirNames {
|
||||
if (global.DIR_NAMES) return global.DIR_NAMES;
|
||||
|
||||
const ROOT_DIR = process.cwd();
|
||||
const SRC_DIR = path.join(ROOT_DIR, "src");
|
||||
const PAGES_DIR = path.join(SRC_DIR, "pages");
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
export default function isDevelopment() {
|
||||
const config = global.CONFIG;
|
||||
|
||||
if (process.env.NODE_ENV == "production") {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.development) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return Boolean(global.CONFIG?.development);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { realpathSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default function isSafePath({
|
||||
filePath,
|
||||
allowedDir,
|
||||
}: {
|
||||
filePath: string;
|
||||
allowedDir: string;
|
||||
}): boolean {
|
||||
const resolved = path.resolve(filePath);
|
||||
if (!resolved.startsWith(allowedDir + path.sep) && resolved !== allowedDir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const real = realpathSync(resolved);
|
||||
return (
|
||||
real.startsWith(allowedDir + path.sep) || real === allowedDir
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user