From fc5b19c661a4c47a28aa6734f0ee95b7db741ec3 Mon Sep 17 00:00:00 2001 From: Marcos Candeia Date: Mon, 28 Oct 2024 12:42:49 -0300 Subject: [PATCH 1/2] Avoid proxying html through asset Signed-off-by: Marcos Candeia --- website/loaders/asset.ts | 41 +++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/website/loaders/asset.ts b/website/loaders/asset.ts index 0d931b173..ef48295bb 100644 --- a/website/loaders/asset.ts +++ b/website/loaders/asset.ts @@ -1,22 +1,53 @@ import { fetchSafe, STALE } from "../../utils/fetch.ts"; -import { shortcircuit } from "@deco/deco"; + interface Props { /** * @description Asset src like: https://fonts.gstatic.com/... */ src: string; } -const loader = async (props: Props) => { + +const loader = async (props: Props, request: Request): Promise => { const url = new URL(props.src); - if (url.protocol === "file:") { - shortcircuit(new Response("Forbidden", { status: 403 })); + + // Whitelist allowed protocols + const allowedProtocols = ["https:", "http:"]; + if (!allowedProtocols.includes(url.protocol)) { + return new Response( + "Forbidden: Only HTTP and HTTPS protocols are allowed", + { status: 403 }, + ); } + const original = await fetchSafe(url.href, STALE); const response = new Response(original.body, original); + + // Check if the request's Accept header includes "text/html" + const acceptHeader = request.headers.get("accept"); + if (acceptHeader && acceptHeader.includes("text/html")) { + return new Response("Forbidden: text/html not accepted", { status: 403 }); + } + + const contentType = response.headers.get("Content-Type"); + if (contentType && contentType.includes("text/html")) { + return new Response("Forbidden: text/html not accepted as a response", { + status: 403, + }); + } + + // Set strict Content-Security-Policy + response.headers.set( + "Content-Security-Policy", + "default-src 'none'; style-src 'unsafe-inline'", + ); + + // Set cache control headers response.headers.set( - "cache-control", + "Cache-Control", "public, s-maxage=15552000, max-age=15552000, immutable", ); + return response; }; + export default loader; From f7882e26d93b0953f6ea12e3f763b5fa5a6f7258 Mon Sep 17 00:00:00 2001 From: Marcos Candeia Date: Mon, 28 Oct 2024 12:45:04 -0300 Subject: [PATCH 2/2] Use forbidden from deco Signed-off-by: Marcos Candeia --- website/loaders/asset.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/website/loaders/asset.ts b/website/loaders/asset.ts index ef48295bb..d883e7b9c 100644 --- a/website/loaders/asset.ts +++ b/website/loaders/asset.ts @@ -1,5 +1,5 @@ +import { forbidden } from "@deco/deco"; import { fetchSafe, STALE } from "../../utils/fetch.ts"; - interface Props { /** * @description Asset src like: https://fonts.gstatic.com/... @@ -13,10 +13,9 @@ const loader = async (props: Props, request: Request): Promise => { // Whitelist allowed protocols const allowedProtocols = ["https:", "http:"]; if (!allowedProtocols.includes(url.protocol)) { - return new Response( - "Forbidden: Only HTTP and HTTPS protocols are allowed", - { status: 403 }, - ); + forbidden({ + message: "Only HTTP and HTTPS protocols are allowed", + }); } const original = await fetchSafe(url.href, STALE); @@ -25,13 +24,15 @@ const loader = async (props: Props, request: Request): Promise => { // Check if the request's Accept header includes "text/html" const acceptHeader = request.headers.get("accept"); if (acceptHeader && acceptHeader.includes("text/html")) { - return new Response("Forbidden: text/html not accepted", { status: 403 }); + forbidden({ + message: "Forbidden: text/html not accepted", + }); } const contentType = response.headers.get("Content-Type"); if (contentType && contentType.includes("text/html")) { - return new Response("Forbidden: text/html not accepted as a response", { - status: 403, + forbidden({ + message: "Forbidden: text/html not accepted as a response", }); }