Add strict security headers to fetch-url endpoint (#1993)
Browse filesIntroduces comprehensive security headers and forces 'text/plain' content type for all responses from the fetch-url API endpoint. This prevents execution of active content and mitigates risks if the endpoint is accessed directly.
src/routes/api/fetch-url/+server.ts
CHANGED
|
@@ -5,6 +5,14 @@ import { isValidUrl } from "$lib/server/urlSafety";
|
|
| 5 |
|
| 6 |
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
| 7 |
const FETCH_TIMEOUT = 30000; // 30 seconds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
export async function GET({ url }) {
|
| 10 |
const targetUrl = url.searchParams.get("url");
|
|
@@ -43,18 +51,17 @@ export async function GET({ url }) {
|
|
| 43 |
}
|
| 44 |
|
| 45 |
// Stream the response back
|
| 46 |
-
|
|
|
|
| 47 |
const contentDisposition = response.headers.get("content-disposition");
|
| 48 |
|
| 49 |
const headers: HeadersInit = {
|
| 50 |
"Content-Type": contentType,
|
| 51 |
"Cache-Control": "public, max-age=3600",
|
|
|
|
|
|
|
| 52 |
};
|
| 53 |
|
| 54 |
-
if (contentDisposition) {
|
| 55 |
-
headers["Content-Disposition"] = contentDisposition;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
// Get the body as array buffer to check size
|
| 59 |
const arrayBuffer = await response.arrayBuffer();
|
| 60 |
|
|
|
|
| 5 |
|
| 6 |
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
| 7 |
const FETCH_TIMEOUT = 30000; // 30 seconds
|
| 8 |
+
const SECURITY_HEADERS: HeadersInit = {
|
| 9 |
+
// Prevent any active content from executing if someone navigates directly to this endpoint.
|
| 10 |
+
"Content-Security-Policy":
|
| 11 |
+
"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'",
|
| 12 |
+
"X-Content-Type-Options": "nosniff",
|
| 13 |
+
"X-Frame-Options": "DENY",
|
| 14 |
+
"Referrer-Policy": "no-referrer",
|
| 15 |
+
};
|
| 16 |
|
| 17 |
export async function GET({ url }) {
|
| 18 |
const targetUrl = url.searchParams.get("url");
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
// Stream the response back
|
| 54 |
+
// Always return as text/plain to prevent any HTML/JS execution
|
| 55 |
+
const contentType = "text/plain; charset=utf-8";
|
| 56 |
const contentDisposition = response.headers.get("content-disposition");
|
| 57 |
|
| 58 |
const headers: HeadersInit = {
|
| 59 |
"Content-Type": contentType,
|
| 60 |
"Cache-Control": "public, max-age=3600",
|
| 61 |
+
...(contentDisposition ? { "Content-Disposition": contentDisposition } : {}),
|
| 62 |
+
...SECURITY_HEADERS,
|
| 63 |
};
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
// Get the body as array buffer to check size
|
| 66 |
const arrayBuffer = await response.arrayBuffer();
|
| 67 |
|