Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions src/routes/api/fetch-url/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { isValidUrl } from "$lib/server/urlSafety";

const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const FETCH_TIMEOUT = 30000; // 30 seconds
const SECURITY_HEADERS: HeadersInit = {
// Prevent any active content from executing if someone navigates directly to this endpoint.
"Content-Security-Policy":
"default-src 'none'; frame-ancestors 'none'; sandbox; script-src 'none'; img-src 'none'; style-src 'none'; connect-src 'none'; media-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "no-referrer",
};

export async function GET({ url }) {
const targetUrl = url.searchParams.get("url");
Expand Down Expand Up @@ -43,18 +51,17 @@ export async function GET({ url }) {
}

// Stream the response back
const contentType = response.headers.get("content-type") || "application/octet-stream";
// Always return as text/plain to prevent any HTML/JS execution
const contentType = "text/plain; charset=utf-8";
const contentDisposition = response.headers.get("content-disposition");

const headers: HeadersInit = {
"Content-Type": contentType,
"Cache-Control": "public, max-age=3600",
...(contentDisposition ? { "Content-Disposition": contentDisposition } : {}),
...SECURITY_HEADERS,
};

if (contentDisposition) {
headers["Content-Disposition"] = contentDisposition;
}

// Get the body as array buffer to check size
const arrayBuffer = await response.arrayBuffer();

Expand Down
Loading