redroid-mrnumber/service/validate.test.ts
Natalie e5733dad6b fix(security): harden trigger service against argv flag smuggling via ref
Security review flagged unsanitized request `ref` flowing into the lookup argv. Defense
in depth: (1) validate ref at the HTTP boundary against a bounded safe charset with no
leading '-' (isValidRef → 400 invalid_ref), and (2) pass both values as the unambiguous
--opt=value form so a token can never be re-parsed as a flag by argparse even if a row
reached the queue unsanitized. Extracted pure validators (cleanPhone/isValidRef) to
validate.ts. 12 service tests (incl. smuggling cases) + typecheck; spawn drain re-smoked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 14:04:26 -04:00

30 lines
1.1 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { cleanPhone, isValidRef } from './validate.ts';
describe('cleanPhone', () => {
test('normalizes spacing/punctuation and keeps a leading +', () => {
expect(cleanPhone('+1 (555) 123-4567')).toBe('+15551234567');
expect(cleanPhone('6315304426')).toBe('6315304426');
});
test('rejects too-short / too-long / non-numeric', () => {
expect(cleanPhone('123')).toBeNull();
expect(cleanPhone('abc')).toBeNull();
expect(cleanPhone('+1234567890123456')).toBeNull(); // 16 digits
});
});
describe('isValidRef (argv-injection hardening)', () => {
test('accepts ordinary correlation ids', () => {
for (const r of ['prospect-9', 'lead_42', 'abc.def:123', 'A1']) expect(isValidRef(r)).toBe(true);
});
test('rejects flag-smuggling and unsafe shapes', () => {
for (const r of ['--device', '-x', '--phone=+1999', 'a b', 'a;rm -rf', '$(whoami)', '']) {
expect(isValidRef(r)).toBe(false);
}
});
test('rejects over-long refs (>128)', () => {
expect(isValidRef('a'.repeat(129))).toBe(false);
expect(isValidRef('a'.repeat(128))).toBe(true);
});
});