feat(16-02): add EML_OBJECT_KEY_REGEX + parameterize B2 key validation (D-05)

- New EML_OBJECT_KEY_REGEX enforces phishing/<id>/<id>.eml, rejects traversal
- presignDownload/presignUpload/downloadToBuffer take optional keyRegex,
  defaulting to OBJECT_KEY_REGEX so existing LogLift call sites are unchanged
- OBJECT_KEY_REGEX itself left untouched (skill-doc rule)
This commit is contained in:
lorentz 2026-07-15 10:23:19 -04:00
parent 6de92a507b
commit 8630fd5151

View file

@ -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<Buffer> {
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(() => '');