{"record":{"id":"a68f5a2d0f830383","repo":"koala73/worldmonitor","slug":"redirect-to-disallowed-domain","errorCode":null,"errorMessage":"Redirect to disallowed domain","messagePattern":"Redirect to disallowed domain","errorType":"http","errorClass":"RssProxyPolicyError","httpStatus":403,"severity":"error","filePath":"api/rss-proxy.js","lineNumber":85,"sourceCode":"    return new URL(feedUrl).hostname === 'news.google.com';\n  } catch {\n    return false;\n  }\n}\n\nfunction assertHttpProtocol(url, message = 'URL protocol not allowed', status = 400) {\n  if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n    throw new RssProxyPolicyError(message, status);\n  }\n}\n\nfunction assertAllowedRedirect(url) {\n  assertHttpProtocol(url, 'Redirect protocol not allowed', 403);\n  // Apply the same www-normalization as the initial domain check so that\n  // canonical redirects (e.g. apex -> www) are not incorrectly rejected when\n  // only one form is in the allowlist.\n  if (!isAllowedDomain(url.hostname)) {\n    throw new RssProxyPolicyError('Redirect to disallowed domain');\n  }\n}\n\nexport default async function handler(req, ctx) {\n  const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS');\n\n  if (isDisallowedOrigin(req)) {\n    return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders);\n  }\n\n  // Handle CORS preflight\n  if (req.method === 'OPTIONS') {\n    return new Response(null, { status: 204, headers: corsHeaders });\n  }\n  if (req.method !== 'GET') {\n    return jsonResponse({ error: 'Method not allowed' }, 405, corsHeaders);\n  }\n","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/rss-proxy.js#L67-L103","documentation":"Thrown by assertAllowedRedirect() in the RSS proxy when a redirect response's Location header points to a hostname not in the domain allowlist (isAllowedDomain). This is the second security guard after the protocol check — it prevents the proxy from following redirects to arbitrary (potentially malicious or internal) domains. The www-normalization applied to the initial domain check is also applied here so canonical apex-to-www redirects are not rejected. The error is an RssProxyPolicyError with HTTP status 403.","triggerScenarios":"An RSS feed URL that is itself allowed (passes the initial check) returns a 301/302/303/307/308 redirect to a hostname that is NOT in the allowlist. For example: a feed at feeds.allowed.com redirects to cdn.disallowed.net/feed.xml. The www-normalization means www.allowed.com and allowed.com are treated equivalently, but a genuinely different domain is blocked.","commonSituations":"A feed source changed its CDN or redirect target to a new domain not yet in the allowlist; a feed was compromised or misconfigured to redirect to an unexpected domain; a legitimately allowed feed moved hosts and the allowlist was not updated.","solutions":["Add the redirect target hostname to the allowlist (isAllowedDomain / ALLOWED_RSS_DOMAINS) if it is a legitimate CDN or mirror of the original feed.","Contact the feed provider to confirm the new redirect target is legitimate before allowlisting.","If the redirect is unexpected/suspicious, do NOT add it — investigate whether the feed was compromised.","Use the Railway relay path for the original domain if the direct redirect chain is problematic (relay-only domains skip direct fetch)."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Before adding a feed, verify its redirect chain stays within allowed domains\nconst ALLOWED_DOMAINS = getAllowlist(); // mirror of isAllowedDomain\nfunction isRedirectSafe(location, currentUrl) {\n  try {\n    const url = new URL(location, currentUrl);\n    return (url.protocol === 'http:' || url.protocol === 'https:')\n      && ALLOWED_DOMAINS.has(wwwNormalize(url.hostname));\n  } catch { return false; }\n}","typeGuard":"function isRssProxyPolicyError(e: unknown): e is { status: number } & Error {\n  return e instanceof Error && (e as any).name === 'RssProxyPolicyError'\n    && e.message === 'Redirect to disallowed domain';\n}","tryCatchPattern":"try {\n  const feed = await fetch('/api/rss-proxy?url=' + encodeURIComponent(feedUrl));\n} catch (e) {\n  if (isRssProxyPolicyError(e)) {\n    // The feed redirected to an un-allowlisted domain\n    // Either add the redirect target to the allowlist or use the Railway relay\n    logRedirectIssue(feedUrl);\n  } else throw e;\n}","preventionTips":["Audit feed redirect targets periodically and add legitimate CDN/mirror domains to the allowlist.","Use www-normalization when checking hostnames so apex<->www redirects are not false positives.","Consider adding chronically-redirect-heavy feeds to RELAY_ONLY_DOMAINS to bypass the direct redirect checks."],"tags":["rss-proxy","security","redirect","allowlist","edge-function"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}