feat: Implement secure static file serving with path traversal protection and XSS prevention for template titles.

This commit is contained in:
syntaxbullet
2026-01-07 12:51:08 +01:00
parent 2a1c4e65ae
commit 894cad91a8
7 changed files with 98 additions and 19 deletions

View File

@@ -1,26 +1,46 @@
import { homeRoute } from "./routes/home";
import { healthRoute } from "./routes/health";
import { file } from "bun";
import { join } from "path";
import { join, resolve } from "path";
export async function router(request: Request): Promise<Response> {
const url = new URL(request.url);
const method = request.method;
// Serve static files from src/web/public
// We treat any path with a dot or starting with specific assets paths as static file candidates
if (url.pathname.includes(".") || url.pathname.startsWith("/public/")) {
// Sanitize path to prevent directory traversal
const safePath = url.pathname.replace(/^(\.\.[\/\\])+/, '');
const filePath = join(import.meta.dir, "public", safePath);
const staticFile = file(filePath);
if (await staticFile.exists()) {
return new Response(staticFile);
}
}
// Resolve the absolute path to the public directory
const publicDir = resolve(import.meta.dir, "public");
if (method === "GET") {
// Handle Static Files
// We handle requests starting with /public/ OR containing an extension (like /style.css)
if (url.pathname.startsWith("/public/") || url.pathname.includes(".")) {
// Normalize path: remove /public prefix if present so that
// /public/style.css and /style.css both map to .../public/style.css
const relativePath = url.pathname.replace(/^\/public/, "");
// Resolve full path
// We use join with relativePath. If relativePath starts with /, join handles it correctly
// effectively treating it as a segment.
// However, to be extra safe with 'resolve', we ensure we are resolving from publicDir.
// simple join(publicDir, relativePath) is usually enough with 'bun'.
// But we use 'resolve' to handle .. segments correctly.
// We prepend '.' to relativePath to ensure it's treated as relative to publicDir logic
const normalizedRelative = relativePath.startsWith("/") ? "." + relativePath : relativePath;
const requestedPath = resolve(publicDir, normalizedRelative);
// Security Check: Block Path Traversal
if (requestedPath.startsWith(publicDir)) {
const staticFile = file(requestedPath);
if (await staticFile.exists()) {
return new Response(staticFile);
}
} else {
// If path traversal detected, return 403 or 404.
// 403 indicates we caught them.
return new Response("Forbidden", { status: 403 });
}
}
if (url.pathname === "/" || url.pathname === "/index.html") {
return homeRoute();
}