Files
worldmonitor/api/_api-key.js
Pranav Garg b793a61c87 fix(api): harden IP extraction, input validation, redirect SSRF check, and origin-pattern parity (#1013)
- register-interest.js: coerce source/appVersion to string with a 100-char cap
  before forwarding to Convex. Non-string values (objects, arrays) are truthy so
  the previous || 'unknown' guard passed them through, causing Convex to throw
  a type-validation error and surface a 500 to the caller. Also fixes unbounded
  metadata strings filling the registrations table cheaply.

- rss-proxy.js: apply the same www-normalization used by the initial domain check
  to the 301-redirect hostname check. The old bare ALLOWED_DOMAINS.includes(hostname)
  call rejected canonical redirects (e.g. bbc.co.uk -> www.bbc.co.uk) even when
  one form is allowlisted, breaking several feeds silently.

- _api-key.js: align BROWSER_ORIGIN_PATTERNS Vercel-preview regex with the
  narrower pattern already enforced by _cors.js (worldmonitor-*-elie-*.vercel.app).
  The broader worldmonitor-*.vercel.app pattern was dead code because _cors.js
  rejects those origins before _api-key.js is reached.
2026-03-05 07:18:59 +04:00

67 lines
2.4 KiB
JavaScript

const DESKTOP_ORIGIN_PATTERNS = [
/^https?:\/\/tauri\.localhost(:\d+)?$/,
/^https?:\/\/[a-z0-9-]+\.tauri\.localhost(:\d+)?$/i,
/^tauri:\/\/localhost$/,
/^asset:\/\/localhost$/,
];
const BROWSER_ORIGIN_PATTERNS = [
/^https:\/\/(.*\.)?worldmonitor\.app$/,
/^https:\/\/worldmonitor-[a-z0-9-]+-elie-[a-z0-9]+\.vercel\.app$/,
...(process.env.NODE_ENV === 'production' ? [] : [
/^https?:\/\/localhost(:\d+)?$/,
/^https?:\/\/127\.0\.0\.1(:\d+)?$/,
]),
];
function isDesktopOrigin(origin) {
return Boolean(origin) && DESKTOP_ORIGIN_PATTERNS.some(p => p.test(origin));
}
function isTrustedBrowserOrigin(origin) {
return Boolean(origin) && BROWSER_ORIGIN_PATTERNS.some(p => p.test(origin));
}
function extractOriginFromReferer(referer) {
if (!referer) return '';
try {
return new URL(referer).origin;
} catch {
return '';
}
}
export function validateApiKey(req) {
const key = req.headers.get('X-WorldMonitor-Key');
// Same-origin browser requests don't send Origin (per CORS spec).
// Fall back to Referer to identify trusted same-origin callers.
const origin = req.headers.get('Origin') || extractOriginFromReferer(req.headers.get('Referer')) || '';
// Desktop app — always require API key
if (isDesktopOrigin(origin)) {
if (!key) return { valid: false, required: true, error: 'API key required for desktop access' };
const validKeys = (process.env.WORLDMONITOR_VALID_KEYS || '').split(',').filter(Boolean);
if (!validKeys.includes(key)) return { valid: false, required: true, error: 'Invalid API key' };
return { valid: true, required: true };
}
// Trusted browser origin (worldmonitor.app, Vercel previews, localhost dev) — no key needed
if (isTrustedBrowserOrigin(origin)) {
if (key) {
const validKeys = (process.env.WORLDMONITOR_VALID_KEYS || '').split(',').filter(Boolean);
if (!validKeys.includes(key)) return { valid: false, required: true, error: 'Invalid API key' };
}
return { valid: true, required: false };
}
// Explicit key provided from unknown origin — validate it
if (key) {
const validKeys = (process.env.WORLDMONITOR_VALID_KEYS || '').split(',').filter(Boolean);
if (!validKeys.includes(key)) return { valid: false, required: true, error: 'Invalid API key' };
return { valid: true, required: true };
}
// No origin, no key — require API key (blocks unauthenticated curl/scripts)
return { valid: false, required: true, error: 'API key required' };
}