diff --git a/backend/src/services/apiServer.ts b/backend/src/services/apiServer.ts index 1c46d6e..d3e8364 100644 --- a/backend/src/services/apiServer.ts +++ b/backend/src/services/apiServer.ts @@ -111,6 +111,25 @@ const ALLOWED_SCREENER_SECTORS = new Set([ 'Basic Materials', ]); +const NEWS_SYMBOL_PATTERN = /^[A-Z0-9][A-Z0-9./-]{0,15}$/; +const MAX_NEWS_SYMBOLS = 10; + +const normalizeNewsSymbolsQuery = (value: unknown): string => { + const raw = String(value || '').trim(); + if (!raw) return ''; + + const symbols = raw + .split(',') + .map((symbol) => symbol.trim().toUpperCase()) + .filter(Boolean); + + if (symbols.length > MAX_NEWS_SYMBOLS || symbols.some((symbol) => !NEWS_SYMBOL_PATTERN.test(symbol))) { + throw new Error('Invalid news symbols query'); + } + + return symbols.join(','); +}; + interface TradeAuditEvent { event: string; userId?: string; @@ -2726,7 +2745,12 @@ RULES: // ── News: proxy to Alpaca /v1beta1/news ─────────────────────────────── this.app.get('/api/news', this.requireAuth, async (req, res) => { try { - const symbols = String(req.query.symbols || '').trim().toUpperCase(); + let symbols = ''; + try { + symbols = normalizeNewsSymbolsQuery(req.query.symbols); + } catch (validationError: any) { + return res.status(400).json({ error: validationError.message }); + } const limit = Math.max(1, Math.min(50, Number(req.query.limit) || 10)); const alpacaKey = config.ALPACA_API_KEY; const alpacaSecret = config.ALPACA_API_SECRET; diff --git a/backend/verifyMarketDataEndpoints.ts b/backend/verifyMarketDataEndpoints.ts index 23388cb..39353b7 100644 --- a/backend/verifyMarketDataEndpoints.ts +++ b/backend/verifyMarketDataEndpoints.ts @@ -113,6 +113,18 @@ function testAlpacaProxyContracts() { /this\.app\.get\('\/api\/news'[\s\S]*Math\.max\(1, Math\.min\(50, Number\(req\.query\.limit\) \|\| 10\)\)/, '/api/news must clamp limit to 1..50', ); + assertSourceIncludes( + 'const NEWS_SYMBOL_PATTERN = /^[A-Z0-9][A-Z0-9./-]{0,15}$/;', + '/api/news must constrain symbols to expected characters and length', + ); + assertSourceIncludes( + 'const MAX_NEWS_SYMBOLS = 10;', + '/api/news must limit the number of symbols sent upstream', + ); + assertSourceMatches( + /symbols = normalizeNewsSymbolsQuery\(req\.query\.symbols\)[\s\S]*status\(400\)\.json\(\{ error: validationError\.message \}\)/, + '/api/news must reject invalid symbol queries before proxying to Alpaca', + ); assertSourceIncludes( 'https://data.alpaca.markets/v1beta1/news?${qs.toString()}', '/api/news must proxy Alpaca news',