mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-26 17:45:09 +02:00
Harden CORS, XSS, and input validation across all API endpoints and components
- Add CORS origin allowlist (api/_cors.js) replacing Access-Control-Allow-Origin: * - Add isDisallowedOrigin guard to all API endpoints (acled, cloudflare-outages, finnhub, fred-data, hackernews, wingbits) - Gut debug-env endpoint to return 404 - Tighten sanitizeUrl() with escapeAttr output and strict relative URL validation - Add sanitizeUrl() adoption in CountryIntelModal, InsightsPanel, PredictionPanel, RegulationPanel, TechEventsPanel - Comprehensive escapeHtml() hardening in MapPopup (cables, flights, vessels, clusters) - Bound HackerNews concurrent fetches (MAX_CONCURRENCY=10), validate story type and limit params - Add wingbits cache eviction (MAX_LOCAL_CACHE_ENTRIES=2000, sweep on TTL + LRU) - Fix arxiv http→https, og-story parseInt safety with Number.isFinite + clamping
This commit is contained in:
@@ -1,10 +1,33 @@
|
||||
// Wingbits API proxy - keeps API key server-side
|
||||
// Note: Edge runtime is stateless - caching happens client-side and via HTTP Cache-Control
|
||||
import { getCorsHeaders, isDisallowedOrigin } from '../_cors.js';
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
export default async function handler(req) {
|
||||
const url = new URL(req.url);
|
||||
const path = url.pathname.replace('/api/wingbits', '');
|
||||
const corsHeaders = getCorsHeaders(req, 'GET, POST, OPTIONS');
|
||||
|
||||
// Handle CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
if (isDisallowedOrigin(req)) {
|
||||
return new Response(null, {
|
||||
status: 403,
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDisallowedOrigin(req)) {
|
||||
return Response.json({ error: 'Origin not allowed' }, {
|
||||
status: 403,
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
// Get API key from server-side env
|
||||
const apiKey = process.env.WINGBITS_API_KEY;
|
||||
@@ -15,19 +38,7 @@ export default async function handler(req) {
|
||||
configured: false
|
||||
}, {
|
||||
status: 200,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
});
|
||||
}
|
||||
|
||||
// Handle CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
},
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +61,7 @@ export default async function handler(req) {
|
||||
icao24,
|
||||
}, {
|
||||
status: response.status,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,7 +69,7 @@ export default async function handler(req) {
|
||||
|
||||
return Response.json(data, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
...corsHeaders,
|
||||
'Cache-Control': 'public, max-age=86400', // 24h - aircraft details rarely change
|
||||
},
|
||||
});
|
||||
@@ -68,7 +79,7 @@ export default async function handler(req) {
|
||||
icao24,
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -82,7 +93,7 @@ export default async function handler(req) {
|
||||
if (!Array.isArray(icao24List) || icao24List.length === 0) {
|
||||
return Response.json({ error: 'icao24s array required' }, {
|
||||
status: 400,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -124,7 +135,7 @@ export default async function handler(req) {
|
||||
requested: limitedList.length,
|
||||
}, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
...corsHeaders,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -132,7 +143,7 @@ export default async function handler(req) {
|
||||
error: `Batch lookup failed: ${error.message}`,
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -151,7 +162,7 @@ export default async function handler(req) {
|
||||
if (!la || !lo) {
|
||||
return Response.json({ error: 'lat (la) and lon (lo) required' }, {
|
||||
status: 400,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -173,7 +184,7 @@ export default async function handler(req) {
|
||||
details: errorText,
|
||||
}, {
|
||||
status: response.status,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -182,7 +193,7 @@ export default async function handler(req) {
|
||||
|
||||
return Response.json(data, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
...corsHeaders,
|
||||
'Cache-Control': 'public, max-age=30', // 30 seconds - live data
|
||||
},
|
||||
});
|
||||
@@ -192,7 +203,7 @@ export default async function handler(req) {
|
||||
error: `Fetch failed: ${error.message}`,
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -206,7 +217,7 @@ export default async function handler(req) {
|
||||
if (!Array.isArray(areas) || areas.length === 0) {
|
||||
return Response.json({ error: 'areas array required' }, {
|
||||
status: 400,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -238,7 +249,7 @@ export default async function handler(req) {
|
||||
details: errorText,
|
||||
}, {
|
||||
status: response.status,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -247,7 +258,7 @@ export default async function handler(req) {
|
||||
|
||||
return Response.json(data, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
...corsHeaders,
|
||||
'Cache-Control': 'public, max-age=30',
|
||||
},
|
||||
});
|
||||
@@ -257,7 +268,7 @@ export default async function handler(req) {
|
||||
error: `Fetch failed: ${error.message}`,
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -273,7 +284,7 @@ export default async function handler(req) {
|
||||
...data,
|
||||
configured: true,
|
||||
}, {
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
} catch (error) {
|
||||
return Response.json({
|
||||
@@ -281,13 +292,13 @@ export default async function handler(req) {
|
||||
configured: true,
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ error: 'Not found' }, {
|
||||
status: 404,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user