Security hardening for EIA and USASpending features

Fixes identified by red-team audit:

EIA API Proxy:
- Restrict CORS to allowed origins only (HIGH)
- Add HTTP method validation - GET/OPTIONS only (MEDIUM)
- Remove error message information leakage (HIGH)

USASpending Service:
- Add input validation bounds for daysBack (1-90) and limit (1-50)

EconomicPanel:
- Escape all dynamic values in templates (XSS prevention)
- Escape numeric values, trend colors, icons, dates
This commit is contained in:
Elie Habib
2026-01-16 16:18:41 +04:00
parent 5bbe126484
commit 7ecb1b1597
3 changed files with 56 additions and 18 deletions

View File

@@ -2,7 +2,29 @@
// Keeps API key server-side
export const config = { runtime: 'edge' };
const ALLOWED_ORIGINS = [
'https://worldmonitor.app',
'https://www.worldmonitor.app',
'http://localhost:5173',
'http://localhost:3000',
];
function getCorsOrigin(req) {
const origin = req.headers.get('origin') || '';
return ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
}
export default async function handler(req) {
const corsOrigin = getCorsOrigin(req);
// Only allow GET and OPTIONS methods
if (req.method !== 'GET' && req.method !== 'OPTIONS') {
return Response.json({ error: 'Method not allowed' }, {
status: 405,
headers: { 'Access-Control-Allow-Origin': corsOrigin },
});
}
const url = new URL(req.url);
const path = url.pathname.replace('/api/eia', '');
@@ -12,10 +34,9 @@ export default async function handler(req) {
return Response.json({
error: 'EIA API not configured',
configured: false,
message: 'Get a free API key at https://www.eia.gov/opendata/',
}, {
status: 503,
headers: { 'Access-Control-Allow-Origin': '*' },
headers: { 'Access-Control-Allow-Origin': corsOrigin },
});
}
@@ -24,7 +45,7 @@ export default async function handler(req) {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Origin': corsOrigin,
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
@@ -34,7 +55,7 @@ export default async function handler(req) {
// Health check
if (path === '/health' || path === '') {
return Response.json({ configured: true }, {
headers: { 'Access-Control-Allow-Origin': '*' },
headers: { 'Access-Control-Allow-Origin': corsOrigin },
});
}
@@ -90,22 +111,23 @@ export default async function handler(req) {
return Response.json(results, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Origin': corsOrigin,
'Cache-Control': 'public, max-age=1800', // 30 min cache
},
});
} catch (error) {
console.error('[EIA] Fetch error:', error);
return Response.json({
error: `EIA fetch failed: ${error.message}`,
error: 'Failed to fetch EIA data',
}, {
status: 500,
headers: { 'Access-Control-Allow-Origin': '*' },
headers: { 'Access-Control-Allow-Origin': corsOrigin },
});
}
}
return Response.json({ error: 'Not found' }, {
status: 404,
headers: { 'Access-Control-Allow-Origin': '*' },
headers: { 'Access-Control-Allow-Origin': corsOrigin },
});
}