fix: round 2 S3 audit — CRITICAL SigV4 payload hash bug, timeouts, Content-MD5/Length validation
Deploy FileDrop / deploy (push) Successful in 43s
Deploy FileDrop / deploy (push) Successful in 43s
CRITICAL:
- SigV4 canonical request used sha256Hex('') instead of x-amz-content-sha256
header value — every PUT/POST with body would fail 403. Now uses the
signed header value for canonical request, verifyBodyHash after streaming
for integrity.
HIGH:
- Add 30s AbortSignal.timeout to all Telegram CDN fetches in object-stream.ts
(previously could hang indefinitely, exhausting connection pool)
MEDIUM:
- Content-MD5 validation: compute and compare when header is present
- Content-Length validation: reject if actual body size != header
- max-keys=0 clamping: enforce minimum of 1 per S3 spec
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -282,10 +282,11 @@ export const verifySignature = async (
|
||||
return { isValid: false, credential: null, errorCode: 'NotImplemented' };
|
||||
}
|
||||
|
||||
// H4: Compute hash from actual body instead of trusting header blindly.
|
||||
// For streaming bodies (body === null), we cannot hash at this point —
|
||||
// the caller (controller) must verify body hash after streaming.
|
||||
const hashedPayload = await getHashedPayload(body);
|
||||
// CRITICAL: Use the x-amz-content-sha256 header value in the canonical
|
||||
// request because that's what the client signed. The actual body hash is
|
||||
// verified by verifyBodyHash() after streaming, ensuring integrity without
|
||||
// breaking SigV4.
|
||||
const hashedPayload = contentSha256 || (await getHashedPayload(body));
|
||||
|
||||
const canonicalRequest = buildCanonicalRequest(
|
||||
method,
|
||||
|
||||
@@ -60,8 +60,10 @@ const planParts = (parts: ObjectPartSource[], start: number, end: number): Plann
|
||||
const streamFromBytes = (bytes: Uint8Array): ReadableStream<Uint8Array> =>
|
||||
new Response(bytes).body!;
|
||||
|
||||
const TELEGRAM_FETCH_TIMEOUT_MS = 30_000;
|
||||
|
||||
const fetchWholePartBytes = async (telegramUrl: string): Promise<Uint8Array> => {
|
||||
const res = await fetch(telegramUrl);
|
||||
const res = await fetch(telegramUrl, { signal: AbortSignal.timeout(TELEGRAM_FETCH_TIMEOUT_MS) });
|
||||
if (!res.ok) throw new Error(`Telegram fetch failed: ${res.status}`);
|
||||
return new Uint8Array(await res.arrayBuffer());
|
||||
};
|
||||
@@ -77,10 +79,11 @@ const fetchPartBody = async (planned: PlannedPart): Promise<ReadableStream<Uint8
|
||||
}
|
||||
|
||||
const rangeHeader = `bytes=${planned.relativeStart}-${planned.relativeEnd}`;
|
||||
const res = await fetch(
|
||||
planned.part.telegramUrl,
|
||||
wantsWholePart ? undefined : { headers: { range: rangeHeader } },
|
||||
);
|
||||
const fetchOpts: RequestInit = { signal: AbortSignal.timeout(TELEGRAM_FETCH_TIMEOUT_MS) };
|
||||
if (!wantsWholePart) {
|
||||
fetchOpts.headers = { range: rangeHeader };
|
||||
}
|
||||
const res = await fetch(planned.part.telegramUrl, fetchOpts);
|
||||
if (!res.ok) throw new Error(`Telegram fetch failed: ${res.status}`);
|
||||
if (wantsWholePart || res.status === 206) return res.body!;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user