diff --git a/lib/services/b2/client.ts b/lib/services/b2/client.ts index e424201..033ced8 100644 --- a/lib/services/b2/client.ts +++ b/lib/services/b2/client.ts @@ -31,6 +31,16 @@ export const MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024; // 25 MB export const OBJECT_KEY_REGEX = /^[A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+\/eventlogs_[0-9_]+\.json\.gz$/; +/** + * Object-key shape for raw `.eml` evidence uploads (Phase 16 / D-05): + * `phishing/{reportId}/{attachmentId}.eml`. This is a SEPARATE regex from + * OBJECT_KEY_REGEX — per the B2 evidence skill doc, never loosen the + * existing LogLift guard to accommodate a new shape. Path-traversal safe: + * each segment is restricted to `[A-Za-z0-9_-]+`, so `..` cannot appear. + */ +export const EML_OBJECT_KEY_REGEX = + /^phishing\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+\.eml$/; + export class B2NotConfiguredError extends Error { constructor() { super( @@ -145,18 +155,20 @@ function presign(params: PresignParams): string { export function presignDownload( objectKey: string, expiresInSeconds = 600, - cfg: B2Config = getB2Config() + cfg: B2Config = getB2Config(), + keyRegex: RegExp = OBJECT_KEY_REGEX ): string { - if (!OBJECT_KEY_REGEX.test(objectKey)) throw new B2InvalidObjectKeyError(objectKey); + if (!keyRegex.test(objectKey)) throw new B2InvalidObjectKeyError(objectKey); return presign({ method: 'GET', objectKey, expiresInSeconds, config: cfg }); } export function presignUpload( objectKey: string, expiresInSeconds = 1800, - cfg: B2Config = getB2Config() + cfg: B2Config = getB2Config(), + keyRegex: RegExp = OBJECT_KEY_REGEX ): string { - if (!OBJECT_KEY_REGEX.test(objectKey)) throw new B2InvalidObjectKeyError(objectKey); + if (!keyRegex.test(objectKey)) throw new B2InvalidObjectKeyError(objectKey); return presign({ method: 'PUT', objectKey, expiresInSeconds, config: cfg }); } @@ -166,9 +178,10 @@ export function presignUpload( */ export async function downloadToBuffer( objectKey: string, - cfg: B2Config = getB2Config() + cfg: B2Config = getB2Config(), + keyRegex: RegExp = OBJECT_KEY_REGEX ): Promise { - const url = presignDownload(objectKey, 600, cfg); + const url = presignDownload(objectKey, 600, cfg, keyRegex); const res = await fetch(url); if (!res.ok) { const text = await res.text().catch(() => '');