fix: security hardening — CORS, auth bypass, origin validation & bump v2.2.7

- Tighten CORS regex to block worldmonitorEVIL.vercel.app spoofing
- Move sidecar /api/local-env-update behind token auth + add key allowlist
- Add postMessage origin/source validation in LiveNewsPanel
- Replace postMessage wildcard '*' targetOrigin with specific origin
- Add isDisallowedOrigin() check to 25 API endpoints missing it
- Migrate gdelt-geo & EIA from custom CORS to shared _cors.js
- Add CORS to firms-fires, stock-index, youtube/live endpoints
- Tighten youtube/embed.js ALLOWED_ORIGINS regex
- Remove 'unsafe-inline' from CSP script-src
- Add iframe sandbox attribute to YouTube embed
- Validate meta-tags URL query params with regex allowlist
This commit is contained in:
Elie Habib
2026-02-15 20:33:20 +04:00
parent a31f81a0fe
commit a9224254a5
43 changed files with 431 additions and 167 deletions

View File

@@ -1,28 +1,22 @@
// EIA (Energy Information Administration) API proxy
// Keeps API key server-side
import { getCorsHeaders, isDisallowedOrigin } from '../_cors.js';
export const config = { runtime: 'edge' };
function getCorsOrigin(req) {
const origin = req.headers.get('origin') || '';
// Allow *.worldmonitor.app and localhost
if (
origin.endsWith('.worldmonitor.app') ||
origin === 'https://worldmonitor.app' ||
origin.startsWith('http://localhost:')
) {
return origin;
}
return 'https://worldmonitor.app';
}
export default async function handler(req) {
const corsOrigin = getCorsOrigin(req);
const cors = getCorsHeaders(req);
if (isDisallowedOrigin(req)) {
return new Response(JSON.stringify({ error: 'Origin not allowed' }), { status: 403, headers: cors });
}
// Only allow GET and OPTIONS methods
if (req.method !== 'GET' && req.method !== 'OPTIONS') {
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: cors });
}
if (req.method !== 'GET') {
return Response.json({ error: 'Method not allowed' }, {
status: 405,
headers: { 'Access-Control-Allow-Origin': corsOrigin },
headers: cors,
});
}
@@ -38,26 +32,14 @@ export default async function handler(req) {
reason: 'EIA_API_KEY not configured',
}, {
status: 200,
headers: { 'Access-Control-Allow-Origin': corsOrigin },
});
}
// Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': corsOrigin,
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
headers: cors,
});
}
// Health check
if (path === '/health' || path === '') {
return Response.json({ configured: true }, {
headers: { 'Access-Control-Allow-Origin': corsOrigin },
headers: cors,
});
}
@@ -113,8 +95,8 @@ export default async function handler(req) {
return Response.json(results, {
headers: {
'Access-Control-Allow-Origin': corsOrigin,
'Cache-Control': 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=300', // 30 min cache
...cors,
'Cache-Control': 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=300',
},
});
} catch (error) {
@@ -123,13 +105,13 @@ export default async function handler(req) {
error: 'Failed to fetch EIA data',
}, {
status: 500,
headers: { 'Access-Control-Allow-Origin': corsOrigin },
headers: cors,
});
}
}
return Response.json({ error: 'Not found' }, {
status: 404,
headers: { 'Access-Control-Allow-Origin': corsOrigin },
headers: cors,
});
}