fix(C4): validate news symbol filters
Normalize and limit /api/news symbols before proxying to Alpaca so only bounded, expected symbol characters reach the upstream news endpoint. Refs: docs/AUDIT_REDESIGN.md item C4. Co-Authored-By: GPT-5 Codex <noreply@openai.com>
This commit is contained in:
parent
9e22f6460c
commit
7c4b08cdd5
@ -111,6 +111,25 @@ const ALLOWED_SCREENER_SECTORS = new Set([
|
|||||||
'Basic Materials',
|
'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 {
|
interface TradeAuditEvent {
|
||||||
event: string;
|
event: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
@ -2726,7 +2745,12 @@ RULES:
|
|||||||
// ── News: proxy to Alpaca /v1beta1/news ───────────────────────────────
|
// ── News: proxy to Alpaca /v1beta1/news ───────────────────────────────
|
||||||
this.app.get('/api/news', this.requireAuth, async (req, res) => {
|
this.app.get('/api/news', this.requireAuth, async (req, res) => {
|
||||||
try {
|
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 limit = Math.max(1, Math.min(50, Number(req.query.limit) || 10));
|
||||||
const alpacaKey = config.ALPACA_API_KEY;
|
const alpacaKey = config.ALPACA_API_KEY;
|
||||||
const alpacaSecret = config.ALPACA_API_SECRET;
|
const alpacaSecret = config.ALPACA_API_SECRET;
|
||||||
|
|||||||
@ -113,6 +113,18 @@ function testAlpacaProxyContracts() {
|
|||||||
/this\.app\.get\('\/api\/news'[\s\S]*Math\.max\(1, Math\.min\(50, Number\(req\.query\.limit\) \|\| 10\)\)/,
|
/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',
|
'/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(
|
assertSourceIncludes(
|
||||||
'https://data.alpaca.markets/v1beta1/news?${qs.toString()}',
|
'https://data.alpaca.markets/v1beta1/news?${qs.toString()}',
|
||||||
'/api/news must proxy Alpaca news',
|
'/api/news must proxy Alpaca news',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user