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:
@@ -903,13 +903,50 @@ const handlePutObject = async (
|
||||
await cleanupTempFile(streamed.tempPath);
|
||||
return s3ErrorResponse(
|
||||
bodyHashError.errorCode || 'BadDigest',
|
||||
'The Content-MD5 or x-amz-content-sha256 you specified did not match what we received.',
|
||||
'The x-amz-content-sha256 you specified did not match what we received.',
|
||||
`/${bucket}/${key}`,
|
||||
400,
|
||||
reqId,
|
||||
);
|
||||
}
|
||||
|
||||
// Content-Length validation: ensure actual body size matches header
|
||||
const contentLengthHeader = headers['content-length'];
|
||||
if (contentLengthHeader) {
|
||||
const declaredLength = Number.parseInt(contentLengthHeader, 10);
|
||||
if (Number.isFinite(declaredLength) && declaredLength !== streamed.sizeBytes) {
|
||||
await cleanupTempFile(streamed.tempPath);
|
||||
return s3ErrorResponse(
|
||||
'IncompleteBody',
|
||||
'You did not provide the number of bytes specified by the Content-Length HTTP header.',
|
||||
`/${bucket}/${key}`,
|
||||
400,
|
||||
reqId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Content-MD5 validation: verify MD5 when Content-MD5 header is present
|
||||
const contentMd5 = headers['content-md5'];
|
||||
if (contentMd5) {
|
||||
const computedMd5 = Buffer.from(
|
||||
await crypto.subtle.digest(
|
||||
'MD5',
|
||||
new Uint8Array(await Bun.file(streamed.tempPath).arrayBuffer()),
|
||||
),
|
||||
).toString('base64');
|
||||
if (contentMd5 !== computedMd5) {
|
||||
await cleanupTempFile(streamed.tempPath);
|
||||
return s3ErrorResponse(
|
||||
'BadDigest',
|
||||
'The Content-MD5 you specified did not match what we received.',
|
||||
`/${bucket}/${key}`,
|
||||
400,
|
||||
reqId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// M12: Reject oversized bodies
|
||||
if (streamed.sizeBytes > config.maxRequestBodyBytes) {
|
||||
await cleanupTempFile(streamed.tempPath);
|
||||
@@ -1239,7 +1276,10 @@ const handleListObjectsV1 = async (
|
||||
|
||||
const prefix = searchParams.get('prefix') || '';
|
||||
const delimiter = searchParams.get('delimiter') || null;
|
||||
const maxKeys = Math.min(Number.parseInt(searchParams.get('max-keys') || '1000', 10), 1000);
|
||||
const maxKeys = Math.max(
|
||||
1,
|
||||
Math.min(Number.parseInt(searchParams.get('max-keys') || '1000', 10), 1000),
|
||||
);
|
||||
const marker = searchParams.get('marker') || null;
|
||||
const encodingType = searchParams.get('encoding-type') || null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user