Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ All notable changes to World Monitor are documented here.
- PortWatch, CorridorRisk, and transit seed loops on Railway relay (#1560)
- R2 trace storage for forecast debugging with Cloudflare API upload (#1655)

### Security

- CDN-Cache-Control header now only set for trusted origins (worldmonitor.app, Vercel previews, Tauri); no-origin server-side requests always reach the edge function so `validateApiKey` can run, closing a potential cache-bypass path for external scrapers

### Fixed

- Trade Policy panel WTO gate changed from panel-wide to per-tab, so Revenue tab works on desktop without WTO API key (#1663)
Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ All notable changes to World Monitor are documented here. Subscribe via [RSS](/c
- R2 trace storage for forecast debugging with Cloudflare API upload (#1655)
- `@ts-nocheck` injection in Makefile generate target for CI proto-freshness parity (#1637)

### Security

- **CDN cache bypass closed**: `CDN-Cache-Control` header now only emitted for trusted origins (worldmonitor.app, Vercel previews, Tauri). No-origin server-side requests always invoke the edge function so `validateApiKey` runs, preventing a cached 200 from being served to external scrapers.

### Fixed

- Trade Policy panel WTO gate changed from panel-wide to per-tab, so Revenue tab works on desktop without WTO API key (#1663)
Expand Down
2 changes: 1 addition & 1 deletion server/cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const ALLOWED_ORIGIN_PATTERNS: RegExp[] =
? PRODUCTION_PATTERNS
: [...PRODUCTION_PATTERNS, ...DEV_PATTERNS];

function isAllowedOrigin(origin: string): boolean {
export function isAllowedOrigin(origin: string): boolean {
return Boolean(origin) && ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin));
}

Expand Down
10 changes: 8 additions & 2 deletions server/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import { createRouter, type RouteDescriptor } from './router';
import { getCorsHeaders, isDisallowedOrigin } from './cors';
import { getCorsHeaders, isDisallowedOrigin, isAllowedOrigin } from './cors';
// @ts-expect-error — JS module, no declaration file
import { validateApiKey } from '../api/_api-key.js';
import { mapErrorToResponse } from './error-mapper';
Expand Down Expand Up @@ -362,7 +362,13 @@ export function createDomainGateway(
const envOverride = process.env[`CACHE_TIER_OVERRIDE_${rpcName.replace(/-/g, '_').toUpperCase()}`] as CacheTier | undefined;
const tier = (envOverride && envOverride in TIER_HEADERS ? envOverride : null) ?? RPC_CACHE_TIER[pathname] ?? 'medium';
mergedHeaders.set('Cache-Control', TIER_HEADERS[tier]);
const cdnCache = TIER_CDN_CACHE[tier];
// Only allow Vercel CDN caching for trusted origins (worldmonitor.app, Vercel previews,
// Tauri). No-origin server-side requests (external scrapers) must always reach the edge
// function so the auth check in validateApiKey() can run. Without this guard, a cached
// 200 from a trusted-origin browser request could be served to a no-origin scraper,
// bypassing auth entirely.
const reqOrigin = request.headers.get('origin') || '';
const cdnCache = isAllowedOrigin(reqOrigin) ? TIER_CDN_CACHE[tier] : null;
if (cdnCache) mergedHeaders.set('CDN-Cache-Control', cdnCache);
mergedHeaders.set('X-Cache-Tier', tier);

Expand Down
Loading