diff --git a/api/og-story.js b/api/og-story.js index 63b48dd6e..5b1f114e6 100644 --- a/api/og-story.js +++ b/api/og-story.js @@ -25,12 +25,17 @@ const LEVEL_LABELS = { low: 'LOW RISK', }; +function normalizeLevel(rawLevel) { + const level = String(rawLevel || '').toLowerCase(); + return Object.prototype.hasOwnProperty.call(LEVEL_COLORS, level) ? level : 'normal'; +} + export default function handler(req, res) { const url = new URL(req.url, `https://${req.headers.host}`); const countryCode = (url.searchParams.get('c') || '').toUpperCase(); const type = url.searchParams.get('t') || 'ciianalysis'; const score = url.searchParams.get('s'); - const level = url.searchParams.get('l') || 'normal'; + const level = normalizeLevel(url.searchParams.get('l')); const countryName = COUNTRY_NAMES[countryCode] || countryCode || 'Global'; const levelColor = LEVEL_COLORS[level] || '#eab308'; diff --git a/api/og-story.test.mjs b/api/og-story.test.mjs new file mode 100644 index 000000000..d398d529e --- /dev/null +++ b/api/og-story.test.mjs @@ -0,0 +1,48 @@ +import { strict as assert } from 'node:assert'; +import test from 'node:test'; +import handler from './og-story.js'; + +function renderOgStory(query = '') { + const req = { + url: `https://worldmonitor.app/api/og-story${query ? `?${query}` : ''}`, + headers: { host: 'worldmonitor.app' }, + }; + + let statusCode = 0; + let body = ''; + const headers = {}; + + const res = { + setHeader(name, value) { + headers[String(name).toLowerCase()] = String(value); + }, + status(code) { + statusCode = code; + return this; + }, + send(payload) { + body = String(payload); + }, + }; + + handler(req, res); + return { statusCode, body, headers }; +} + +test('normalizes unsupported level values to prevent SVG script injection', () => { + const injectedLevel = encodeURIComponent(''); + const response = renderOgStory(`c=US&s=50&l=${injectedLevel}`); + + assert.equal(response.statusCode, 200); + assert.equal(/