From e6d89050a7c43b79ec3a0d27c89be3f8dc9328ac Mon Sep 17 00:00:00 2001 From: Samuel Wein Date: Thu, 23 Apr 2026 10:41:44 +0200 Subject: [PATCH 1/2] add function to handle redirection of xsl schemas --- .../functions/edge-functions/xml-proxy.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .netlify/functions/edge-functions/xml-proxy.ts diff --git a/.netlify/functions/edge-functions/xml-proxy.ts b/.netlify/functions/edge-functions/xml-proxy.ts new file mode 100644 index 0000000..2e7015e --- /dev/null +++ b/.netlify/functions/edge-functions/xml-proxy.ts @@ -0,0 +1,59 @@ +// Netlify helpfully redirects www.* request to the non-www version. This breaks resolving XSLs for our schemas if strict CORS is used. +// the entries in _redirects don't solve this since it happens at the edge, instead rewrite the request to not use a 301 redirect + +export default async (request: Request) => { + const url = new URL(request.url); + const path = url.pathname; + + let upstream: string | null = null; + + if (path.startsWith("/xml-stylesheet/")) { + const rest = path.slice("/xml-stylesheet/".length); + upstream = + "https://raw.githubusercontent.com/OpenMS/OpenMS/develop/share/OpenMS/XSL/" + + rest; + } else if (path.startsWith("/xml-schema/")) { + const rest = path.slice("/xml-schema/".length); + upstream = + "https://raw.githubusercontent.com/OpenMS/OpenMS/develop/share/OpenMS/SCHEMAS/" + + rest; + } + + if (!upstream) { + return; + } + + const upstreamRes = await fetch(upstream, { + headers: { "User-Agent": "openms-netlify-edge" }, + }); + + if (!upstreamRes.ok) { + return new Response(`Upstream error: ${upstreamRes.status}`, { + status: upstreamRes.status, + headers: { + "Access-Control-Allow-Origin": "*", + "Vary": "Origin", + }, + }); + } + + const headers = new Headers(upstreamRes.headers); + + headers.set("Access-Control-Allow-Origin", "*"); + headers.set("Vary", "Origin"); + headers.set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS"); + if (path.startsWith("/xml-stylesheet/")) { + headers.set("Content-Type", "text/xsl; charset=utf-8"); + } else if (path.endsWith(".xsd")) { + headers.set("Content-Type", "application/xml; charset=utf-8"); + } + + return new Response(upstreamRes.body, { + status: 200, + headers, + }); +}; + +export const config = { + path: ["/xml-stylesheet/*", "/xml-schema/*"], +}; \ No newline at end of file From 7a952fe4361f97a9c2a32fe0c5b9f7337225a9fc Mon Sep 17 00:00:00 2001 From: Samuel Wein Date: Thu, 23 Apr 2026 11:14:37 +0200 Subject: [PATCH 2/2] address review comments --- .../functions/edge-functions/xml-proxy.ts | 68 +++++++++++++++---- netlify.toml | 3 + 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/.netlify/functions/edge-functions/xml-proxy.ts b/.netlify/functions/edge-functions/xml-proxy.ts index 2e7015e..2a46f5d 100644 --- a/.netlify/functions/edge-functions/xml-proxy.ts +++ b/.netlify/functions/edge-functions/xml-proxy.ts @@ -1,6 +1,27 @@ // Netlify helpfully redirects www.* request to the non-www version. This breaks resolving XSLs for our schemas if strict CORS is used. // the entries in _redirects don't solve this since it happens at the edge, instead rewrite the request to not use a 301 redirect +/** Validate and normalise path segments from the request suffix. + * Returns the joined safe path string, or null if any segment is invalid. */ +function validatePathSegments(rest: string): string | null { + const segments = rest.split("/"); + for (const seg of segments) { + // Reject empty segments, dot-traversal, and any percent-encoded slash (%2F / %2f) + if (seg === "" || seg === "." || seg === "..") return null; + if (/%2f/i.test(seg)) return null; + let decoded: string; + try { + decoded = decodeURIComponent(seg); + } catch { + return null; + } + // After decoding, reject traversal components that sneak through encoding + if (decoded === "" || decoded === "." || decoded === "..") return null; + if (decoded.includes("/")) return null; + } + return segments.join("/"); +} + export default async (request: Request) => { const url = new URL(request.url); const path = url.pathname; @@ -8,47 +29,64 @@ export default async (request: Request) => { let upstream: string | null = null; if (path.startsWith("/xml-stylesheet/")) { - const rest = path.slice("/xml-stylesheet/".length); + const safe = validatePathSegments(path.slice("/xml-stylesheet/".length)); + if (!safe) { + return new Response("Bad Request: invalid path", { status: 400 }); + } upstream = "https://raw.githubusercontent.com/OpenMS/OpenMS/develop/share/OpenMS/XSL/" + - rest; + safe; } else if (path.startsWith("/xml-schema/")) { - const rest = path.slice("/xml-schema/".length); + const safe = validatePathSegments(path.slice("/xml-schema/".length)); + if (!safe) { + return new Response("Bad Request: invalid path", { status: 400 }); + } upstream = "https://raw.githubusercontent.com/OpenMS/OpenMS/develop/share/OpenMS/SCHEMAS/" + - rest; + safe; } if (!upstream) { return; } - const upstreamRes = await fetch(upstream, { - headers: { "User-Agent": "openms-netlify-edge" }, - }); + const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Vary": "Origin", + "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS", + }; + + if (request.method === "OPTIONS") { + return new Response(null, { status: 204, headers: corsHeaders }); + } + + let upstreamRes: Response; + try { + upstreamRes = await fetch(upstream, { + method: request.method === "HEAD" ? "HEAD" : "GET", + headers: { "User-Agent": "openms-netlify-edge" }, + }); + } catch { + return new Response("Bad gateway", { status: 502, headers: corsHeaders }); + } if (!upstreamRes.ok) { return new Response(`Upstream error: ${upstreamRes.status}`, { status: upstreamRes.status, - headers: { - "Access-Control-Allow-Origin": "*", - "Vary": "Origin", - }, + headers: corsHeaders, }); } const headers = new Headers(upstreamRes.headers); - headers.set("Access-Control-Allow-Origin", "*"); - headers.set("Vary", "Origin"); - headers.set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS"); + for (const [k, v] of Object.entries(corsHeaders)) headers.set(k, v); if (path.startsWith("/xml-stylesheet/")) { headers.set("Content-Type", "text/xsl; charset=utf-8"); } else if (path.endsWith(".xsd")) { headers.set("Content-Type", "application/xml; charset=utf-8"); } - return new Response(upstreamRes.body, { + return new Response(request.method === "HEAD" ? null : upstreamRes.body, { status: 200, headers, }); diff --git a/netlify.toml b/netlify.toml index 6dd0cc4..48eb757 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,2 +1,5 @@ +[build] +edge_functions = ".netlify/functions/edge-functions" + [build.environment] HUGO_VERSION = "0.155.0"