Skip to content
Merged
Show file tree
Hide file tree
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
11 changes: 11 additions & 0 deletions src/middleware/cache-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineMiddleware } from "astro:middleware";

export const cacheControl = defineMiddleware(async (context, next) => {
const response = await next();

const isApi = context.url.pathname.startsWith("/api/");
const isGet = context.request.method === "GET";
response.headers.set("Cache-Control", !isGet || isApi ? "no-store" : "public, max-age=600");

return response;
});
6 changes: 3 additions & 3 deletions src/middleware.ts → src/middleware/csp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ declare const HTMLRewriter: new () => {
transform(response: Response): Response;
};

export const onRequest = defineMiddleware(async (context, next) => {
export const csp = defineMiddleware(async (context, next) => {
const nonce = crypto.randomUUID().replace(/-/g, "");
context.locals.cspNonce = nonce;

Expand All @@ -17,7 +17,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
return response;
}

const csp = [
const cspHeader = [
"default-src 'self'",
// 'strict-dynamic' trusts scripts loaded by nonced scripts; removes need for 'unsafe-inline'
`script-src 'nonce-${nonce}' 'strict-dynamic' https://secure.qgiv.com https://plausible.io https://pal-chat.net https://techforpalestine.org/cdn-cgi/`,
Expand Down Expand Up @@ -52,6 +52,6 @@ export const onRequest = defineMiddleware(async (context, next) => {
});

const transformed = rewriter.transform(response);
transformed.headers.set("Content-Security-Policy", csp);
transformed.headers.set("Content-Security-Policy", cspHeader);
return transformed;
});
17 changes: 7 additions & 10 deletions src/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { defineMiddleware } from "astro:middleware";
import { sequence } from "astro:middleware";
import { cacheControl } from "./cache-control.js";
import { csp } from "./csp.js";

export const onRequest = defineMiddleware(async (context, next) => {
const response = await next();

const isApi = context.url.pathname.startsWith("/api/");
const isGet = context.request.method === "GET";
response.headers.set("Cache-Control", !isGet || isApi ? "no-store" : "public, max-age=600");

return response;
});
// cacheControl runs first so the header is set on every response.
// csp runs second and may replace the response via HTMLRewriter; the
// cache-control header is preserved on the transformed response.
export const onRequest = sequence(cacheControl, csp);
19 changes: 14 additions & 5 deletions src/pages/api/project-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,28 @@ async function proxy(request: Request, locals: unknown): Promise<Response> {
const url = new URL(request.url);
const path = url.searchParams.get("path");

if (!path || !path.startsWith("/api/method/")) {
// Normalise dot-segments (../, ./) before the prefix check so a crafted
// path like /api/method/../../api/auth/admin cannot bypass the guard.
const normalizedPath = path ? new URL(path, "http://localhost").pathname : null;

if (!normalizedPath || !normalizedPath.startsWith("/api/method/")) {
return new Response(JSON.stringify({ error: "Path not allowed" }), {
status: 403,
headers: { "Content-Type": "application/json" },
});
}

const upstream = `${apiUrl.replace(/\/$/, "")}${path}`;
const upstream = `${apiUrl.replace(/\/$/, "")}${normalizedPath}`;

const headers = new Headers(request.headers);
// Explicit allowlist — never forward cookies, IP headers, or other
// browser-supplied headers that could influence upstream access controls.
const FORWARD_HEADERS = ["content-type", "accept", "accept-language", "accept-encoding"];
const headers = new Headers();
for (const name of FORWARD_HEADERS) {
const val = request.headers.get(name);
if (val) headers.set(name, val);
}
headers.set("Authorization", secretKey);
// Don't forward host header to the upstream
headers.delete("host");

const upstreamResponse = await fetch(upstream, {
method: request.method,
Expand Down
Loading