{"record":{"id":"29002455166492ae","repo":"koala73/worldmonitor","slug":"url-protocol-not-allowed","errorCode":null,"errorMessage":"URL protocol not allowed","messagePattern":"URL protocol not allowed","errorType":"http","errorClass":"RssProxyPolicyError","httpStatus":400,"severity":"error","filePath":"api/rss-proxy.js","lineNumber":227,"sourceCode":"    }),\n  }, timeoutMs);\n}\n\n// Allowlist + match predicate live in api/_rss-allowed-domain-match.js\n// (shared with scripts/validate-rss-feeds.mjs --ci so the SSRF guard runs\n// identically in the Edge handler and the build-time validator).\n\nfunction isGoogleNewsFeedUrl(feedUrl) {\n  try {\n    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);","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/api/rss-proxy.js#L209-L245","documentation":"api/rss-proxy.js is a feed-fetching endpoint with an SSRF guard: before any network call it parses the `url` query parameter and assertHttpProtocol rejects every scheme except http: and https: with HTTP 400. A scheme-less value like `example.com/feed.xml` also lands here because WHATWG URL parsing yields the pseudo-protocol `example.com:`.","triggerScenarios":"Passing a URL without a scheme (url=example.com/rss.xml); passing feed://, ftp://, data: or file: URLs; a caller double-encoding the parameter so the protocol portion is mangled.","commonSituations":"Feed URLs copied from reader apps that use feed://; hand-built query strings that omit https://; test fixtures passing bare hostnames.","solutions":["Always pass a fully-qualified URL: url=https://example.com/rss.xml","URL-encode the feed URL with encodeURIComponent when it carries its own query params","Fix the upstream caller that strips or double-encodes the scheme"],"exampleFix":"# before\nGET /api/rss-proxy?url=example.com/feed.xml        # protocol becomes 'example.com:' -> 400\nGET /api/rss-proxy?url=ftp://example.com/feed.xml   # non-http scheme -> 400\n\n# after\nGET /api/rss-proxy?url=https%3A%2F%2Fexample.com%2Ffeed.xml","handlingStrategy":"validation","validationCode":"// Client-side guard before calling the RSS proxy\nfunction validFeedParam(raw) {\n  let u;\n  try { u = new URL(raw); } catch { return false; }\n  return u.protocol === 'http:' || u.protocol === 'https:';\n}\nif (!validFeedParam(feedUrl)) throw new Error(`Feed URL must be absolute http(s): ${feedUrl}`);","typeGuard":null,"tryCatchPattern":"try {\n  const res = await fetch(`/api/rss-proxy?url=${encodeURIComponent(feedUrl)}`);\n  if (res.status === 400) {/* fix the caller's URL construction; do not retry unchanged */}\n} catch (e) { /* network-level handling */ }","preventionTips":["Construct proxy URLs only from stored, canonical https:// feed addresses","Always encodeURIComponent the url parameter","Run feed configs through a validator (the repo's scripts/validate-rss-feeds.mjs shares the same rules)"],"tags":["url-validation","ssrf","rss-proxy","edge-function"],"backgroundTag":"unsupported-url-scheme","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}