Fix security vulnerabilities
This commit is contained in:
Vendored
+1
-1
@@ -7,7 +7,7 @@ export default function grabConstants() {
|
||||
const ServerDefaultRequestBodyLimitBytes = MB_IN_BYTES * 10;
|
||||
const MaxBundlerRebuilds = 5;
|
||||
const RouteIgnorePatterns = [/\/\(/];
|
||||
if (config.pages_exclude_patterns) {
|
||||
if (config?.pages_exclude_patterns) {
|
||||
RouteIgnorePatterns.push(...config.pages_exclude_patterns);
|
||||
}
|
||||
return {
|
||||
|
||||
Vendored
+2
-1
@@ -19,7 +19,8 @@ export default function grabDirNames() {
|
||||
const BUNEXT_PUBLIC_DIR = path.join(BUNX_CWD_DIR, "public");
|
||||
const HYDRATION_DST_DIR = path.join(BUNEXT_PUBLIC_DIR, "pages");
|
||||
const BUNEXT_VENDOR_DIR = path.join(BUNEXT_PUBLIC_DIR, "vendor");
|
||||
const BUNEXT_CACHE_DIR = path.join(BUNEXT_PUBLIC_DIR, "cache");
|
||||
// Kept outside BUNEXT_PUBLIC_DIR: that directory is served over HTTP.
|
||||
const BUNEXT_CACHE_DIR = path.join(BUNX_CWD_DIR, "cache");
|
||||
const HYDRATION_DST_DIR_MAP_JSON_FILE_NAME = "map.json";
|
||||
const HYDRATION_DST_DIR_MAP_JSON_FILE = path.join(HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE_NAME);
|
||||
const BUNX_ROOT_DIR = path.resolve(__dirname, "../../");
|
||||
|
||||
Vendored
+9
-1
@@ -2,6 +2,14 @@ import type { BunxRouteParams } from "../types";
|
||||
type Params = {
|
||||
req: Request;
|
||||
query?: any;
|
||||
/**
|
||||
* Maximum request body size. Defaults to
|
||||
* `ServerDefaultRequestBodyLimitBytes`.
|
||||
*/
|
||||
max_body_bytes?: number;
|
||||
};
|
||||
export default function grabRouteParams({ req, query: passed_query, }: Params): Promise<BunxRouteParams>;
|
||||
/**
|
||||
* Throws `RequestBodyTooLargeError` if the body exceeds `max_body_bytes`.
|
||||
*/
|
||||
export default function grabRouteParams({ req: initial_req, query: passed_query, max_body_bytes, }: Params): Promise<BunxRouteParams>;
|
||||
export {};
|
||||
|
||||
Vendored
+28
-4
@@ -1,11 +1,35 @@
|
||||
import _ from "lodash";
|
||||
import deserializeQuery from "./deserialize-query";
|
||||
export default async function grabRouteParams({ req, query: passed_query, }) {
|
||||
const url = new URL(req.url);
|
||||
import grabConstants from "./grab-constants";
|
||||
import readRequestBody from "./read-request-body";
|
||||
/**
|
||||
* Throws `RequestBodyTooLargeError` if the body exceeds `max_body_bytes`.
|
||||
*/
|
||||
export default async function grabRouteParams({ req: initial_req, query: passed_query, max_body_bytes, }) {
|
||||
const url = new URL(initial_req.url);
|
||||
const query = deserializeQuery(Object.fromEntries(url.searchParams));
|
||||
const body = await (async () => {
|
||||
const raw_body = await readRequestBody({
|
||||
req: initial_req,
|
||||
max_bytes: max_body_bytes || grabConstants().ServerDefaultRequestBodyLimitBytes,
|
||||
});
|
||||
// The original body stream is consumed above, so hand handlers a fresh
|
||||
// request over the buffered bytes. This keeps `req.text()`,
|
||||
// `req.formData()`, etc. usable (e.g. for webhook signatures/uploads).
|
||||
const req = raw_body
|
||||
? new Request(initial_req.url, {
|
||||
method: initial_req.method,
|
||||
headers: initial_req.headers,
|
||||
body: raw_body,
|
||||
signal: initial_req.signal,
|
||||
})
|
||||
: initial_req;
|
||||
const body = (() => {
|
||||
if (initial_req.method == "GET")
|
||||
return undefined;
|
||||
if (!raw_body)
|
||||
return {};
|
||||
try {
|
||||
return req.method == "GET" ? undefined : await req.json();
|
||||
return JSON.parse(new TextDecoder().decode(raw_body) || "{}");
|
||||
}
|
||||
catch (error) {
|
||||
return {};
|
||||
|
||||
Vendored
+24
-3
@@ -1,15 +1,36 @@
|
||||
import { realpathSync } from "fs";
|
||||
import path from "path";
|
||||
export default function isSafePath({ filePath, allowedDir, }) {
|
||||
const allowed = path.resolve(allowedDir);
|
||||
const resolved = path.resolve(filePath);
|
||||
if (!resolved.startsWith(allowedDir + path.sep) && resolved !== allowedDir) {
|
||||
if (!isInside(resolved, allowed)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const real = realpathSync(resolved);
|
||||
return (real.startsWith(allowedDir + path.sep) || real === allowedDir);
|
||||
return isInside(realpathSync(resolved), resolveRealDir(allowed));
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function isInside(target, dir) {
|
||||
return target === dir || target.startsWith(dir + path.sep);
|
||||
}
|
||||
const REAL_DIRS = new Map();
|
||||
/**
|
||||
* Resolves symlinks in the allowed directory itself (e.g. a project under a
|
||||
* symlinked path), so real file paths inside it still match.
|
||||
*/
|
||||
function resolveRealDir(dir) {
|
||||
const cached = REAL_DIRS.get(dir);
|
||||
if (cached)
|
||||
return cached;
|
||||
try {
|
||||
const real = realpathSync(dir);
|
||||
REAL_DIRS.set(dir, real);
|
||||
return real;
|
||||
}
|
||||
catch {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
export declare class RequestBodyTooLargeError extends Error {
|
||||
status: number;
|
||||
constructor(max_bytes: number);
|
||||
}
|
||||
type Params = {
|
||||
req: Request;
|
||||
max_bytes: number;
|
||||
};
|
||||
/**
|
||||
* Reads the whole request body, aborting as soon as it grows past
|
||||
* `max_bytes` so oversized (or chunked, length-less) bodies are never fully
|
||||
* buffered. Returns `undefined` when the request has no body.
|
||||
*/
|
||||
export default function readRequestBody({ req, max_bytes, }: Params): Promise<Uint8Array<ArrayBuffer> | undefined>;
|
||||
export {};
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
export class RequestBodyTooLargeError extends Error {
|
||||
status = 413;
|
||||
constructor(max_bytes) {
|
||||
super(`Request body exceeds ${max_bytes} bytes`);
|
||||
this.name = "RequestBodyTooLargeError";
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Reads the whole request body, aborting as soon as it grows past
|
||||
* `max_bytes` so oversized (or chunked, length-less) bodies are never fully
|
||||
* buffered. Returns `undefined` when the request has no body.
|
||||
*/
|
||||
export default async function readRequestBody({ req, max_bytes, }) {
|
||||
if (req.method == "GET" || req.method == "HEAD" || !req.body) {
|
||||
return undefined;
|
||||
}
|
||||
const content_length = Number(req.headers.get("content-length"));
|
||||
if (content_length > max_bytes) {
|
||||
throw new RequestBodyTooLargeError(max_bytes);
|
||||
}
|
||||
const reader = req.body.getReader();
|
||||
const chunks = [];
|
||||
let total = 0;
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done)
|
||||
break;
|
||||
total += value.byteLength;
|
||||
if (total > max_bytes) {
|
||||
await reader.cancel();
|
||||
throw new RequestBodyTooLargeError(max_bytes);
|
||||
}
|
||||
chunks.push(value);
|
||||
}
|
||||
const body = new Uint8Array(total);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
body.set(chunk, offset);
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* # Serialize a value as JSON that is safe to embed in an inline `<script>`
|
||||
*
|
||||
* Escapes `<`, `>`, `&`, U+2028 and U+2029 so the payload can never close
|
||||
* the script tag (`</script>`), open an HTML comment (`<!--`) or otherwise
|
||||
* change the HTML parser state.
|
||||
*/
|
||||
export default function serializeForScript(value: any): string;
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* # Serialize a value as JSON that is safe to embed in an inline `<script>`
|
||||
*
|
||||
* Escapes `<`, `>`, `&`, U+2028 and U+2029 so the payload can never close
|
||||
* the script tag (`</script>`), open an HTML comment (`<!--`) or otherwise
|
||||
* change the HTML parser state.
|
||||
*/
|
||||
export default function serializeForScript(value) {
|
||||
const json = JSON.stringify(value ?? null) ?? "null";
|
||||
return json
|
||||
.replace(/</g, "\\u003c")
|
||||
.replace(/>/g, "\\u003e")
|
||||
.replace(/&/g, "\\u0026")
|
||||
.replace(/\u2028/g, "\\u2028")
|
||||
.replace(/\u2029/g, "\\u2029");
|
||||
}
|
||||
Reference in New Issue
Block a user