Skip to content
Open
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
21 changes: 17 additions & 4 deletions vtex/loaders/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ const buildProxyRoutes = (
const urlToProxy = `https://${hostname}`;
const hostToUse = hostname;

const routeFromPath = (pathTemplate: string): Route => {
const routeFromPath = (
pathTemplate: string,
highPriority?: boolean,
): Route => {
const handlerValue = {
__resolveType: "website/handlers/proxy.ts",
url: urlToProxy,
Expand All @@ -81,14 +84,24 @@ const buildProxyRoutes = (

return ({
pathTemplate,
highPriority,
handler: {
value: handlerValue,
},
});
};
const routesFromPaths = [...PATHS_TO_PROXY, ...extraPaths].map(
routeFromPath,
);
const routesFromPaths = [
// PATHS_TO_PROXY are VTEX system paths — checkout, account, login, /api,
// /_v. They must win over any catch-all: an A/B audience registering `/*`
// with priority scores 1000 + rank("/*") = 1003 and would otherwise
// outrank rank("/checkout") = 6, breaking cart and login.
...PATHS_TO_PROXY.map((path) => routeFromPath(path, true)),
// extraPaths is site configuration and routinely contains its own
// catch-alls (`/*`, `/section/*`) used as a fallback for pages the
// storefront does not implement. Promoting those would let the fallback
// outrank the storefront's own pages and swallow the entire site.
...extraPaths.map((path) => routeFromPath(path)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A configured duplicate of a VTEX system path loses that path’s priority, allowing the A/B catch-all to swallow checkout/login again. Filter paths already in PATHS_TO_PROXY from extraPaths so built-ins remain authoritative.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vtex/loaders/proxy.ts, line 103:

<comment>A configured duplicate of a VTEX system path loses that path’s priority, allowing the A/B catch-all to swallow checkout/login again. Filter paths already in `PATHS_TO_PROXY` from `extraPaths` so built-ins remain authoritative.</comment>

<file context>
@@ -81,19 +84,24 @@ const buildProxyRoutes = (
+      // catch-alls (`/*`, `/section/*`) used as a fallback for pages the
+      // storefront does not implement. Promoting those would let the fallback
+      // outrank the storefront's own pages and swallow the entire site.
+      ...extraPaths.map((path) => routeFromPath(path)),
+    ];
 
</file context>
Suggested change
...extraPaths.map((path) => routeFromPath(path)),
...extraPaths.filter((path) => !PATHS_TO_PROXY.includes(path)).map((path) =>
routeFromPath(path)
),

];

const [include, routes] = generateDecoSiteMap
? [[...(includeSiteMap ?? []), decoSiteMapUrl], [{
Expand Down
35 changes: 35 additions & 0 deletions website/handlers/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ const HOP_BY_HOP = [
const noTrailingSlashes = (str: string) =>
str.at(-1) === "/" ? str.slice(0, -1) : str;
const sanitize = (str: string) => str.startsWith("/") ? str : `/${str}`;
/**
* Canonical form of an IP for comparison only — never for forwarding.
* x-forwarded-for entries may be bracketed and carry a port ([::1]:443,
* 1.2.3.4:56789) and IPv6 hex casing varies between hops; cf-connecting-ip
* is always a bare address.
*/
const normalizeIp = (value: string): string => {
const ip = value.trim().toLowerCase();
const bracketed = ip.match(/^\[(.+)\](?::\d+)?$/);
if (bracketed) return bracketed[1];
const ipv4WithPort = ip.match(/^([\d.]+):\d+$/);
if (ipv4WithPort) return ipv4WithPort[1];
return ip;
};
Comment on lines +26 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Equivalent IPv6 spellings can still be prepended as duplicate client entries because normalizeIp is not actually canonical for IPv6. Canonicalize parsed IPv6 (including mapped forms) before the deduplication comparison while preserving the original forwarded value.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At website/handlers/proxy.ts, line 26:

<comment>Equivalent IPv6 spellings can still be prepended as duplicate client entries because `normalizeIp` is not actually canonical for IPv6. Canonicalize parsed IPv6 (including mapped forms) before the deduplication comparison while preserving the original forwarded value.</comment>

<file context>
@@ -17,6 +17,20 @@ const HOP_BY_HOP = [
+ * 1.2.3.4:56789) and IPv6 hex casing varies between hops; cf-connecting-ip
+ * is always a bare address.
+ */
+const normalizeIp = (value: string): string => {
+  const ip = value.trim().toLowerCase();
+  const bracketed = ip.match(/^\[(.+)\](?::\d+)?$/);
</file context>
Suggested change
const normalizeIp = (value: string): string => {
const ip = value.trim().toLowerCase();
const bracketed = ip.match(/^\[(.+)\](?::\d+)?$/);
if (bracketed) return bracketed[1];
const ipv4WithPort = ip.match(/^([\d.]+):\d+$/);
if (ipv4WithPort) return ipv4WithPort[1];
return ip;
};
const normalizeIp = (value: string): string => {
const ip = value.trim().toLowerCase();
const bracketed = ip.match(/^\[(.+)\](?::\d+)?$/);
const host = bracketed?.[1] ??
ip.match(/^([\d.]+):\d+$/)?.[1] ??
ip;
if (!host.includes(":")) return host;
try {
return new URL(`http://[${host}]`).hostname.slice(1, -1);
} catch {
return host;
}
};

export const removeCFHeaders = (headers: Headers) => {
headers.forEach((_value, key) => {
if (key.startsWith("cf-")) {
Expand Down Expand Up @@ -145,7 +159,28 @@ export default function Proxy({
if (isFreshCtx<DecoSiteState>(_ctx)) {
_ctx?.state?.monitoring?.logger?.log?.("proxy received headers", headers);
}
// cf-connecting-ip carries the real client IP and removeCFHeaders is about
// to drop it, leaving the proxied origin without x-real-ip. x-forwarded-for
// usually already arrives with the client IP first, so only fill the gaps.
//
// Trust boundary: these headers are only as trustworthy as the ingress in
// front of this handler. x-forwarded-for is already forwarded untouched, so
// an origin reachable outside the CDN could always be fed a forged first
// entry — deriving x-real-ip from cf-connecting-ip does not widen that.
// Authenticating the edge belongs at the ingress, not here.
const clientIp = headers.get("cf-connecting-ip");
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
removeCFHeaders(headers); // cf-headers are not ASCII-compliant
if (clientIp) {
const forwardedFor = headers.get("x-forwarded-for");
if (!forwardedFor) {
headers.set("x-forwarded-for", clientIp);
} else if (
normalizeIp(forwardedFor.split(",")[0]) !== normalizeIp(clientIp)
) {
headers.set("x-forwarded-for", `${clientIp}, ${forwardedFor}`);
}
headers.set("x-real-ip", clientIp);
}
if (removeDirtyCookies) {
removeDirtyCookiesFn(headers);
}
Expand Down
Loading