fix: audit S3 protocol — 15+ security & correctness fixes
Deploy FileDrop / deploy (push) Successful in 43s
Deploy FileDrop / deploy (push) Successful in 43s
HIGH severity fixes: - H1: Bot token leak via 302 redirect — always proxy S3 GETs - H2: PUT TOCTOU race — add unique partial index (bucket_id, s3_key) WHERE NOT deleted - H3: GET/HEAD ignore conditional headers (If-Match, If-None-Match, etc.) - H4: Body payload hash not verified — add verifyBodyHash() post-stream check - H5: Header-based auth has no expiry check — add 15-min clock skew window - H7: Multipart abort does not delete parts — DELETE before UPDATE status - H8: CompleteMultipartUpload skips part number & etag verification - H9: XML regex fails on keys containing < — use non-greedy [\s\S]*? - H10: Path-style vs virtual-hosted key decode mismatch MEDIUM severity fixes: - M1: Add Date header fallback for x-amz-date - M2/M3: Validate service/termination in credential scope - M4: Temp file leak when forwardToStorage throws in handleUploadPart - M5: Multipart key consistency check (s3Key matches URL) - M7: Use stored content-type from multipart initiate - M9: Copy conditional headers skip when fileHash is null - M11: Add 1000-key limit on DeleteObjects - M13: Stricter bucket name validation (no .., no IP format) - M14: NaN partNumber bypasses validation LOW fixes: - normalizeUri: dot-segment removal per RFC 3986 - localeCompare -> byte-order comparison in canonical query string - Validate host in signed headers - Server: AmazonS3 header on all responses - x-amz-id-2 separate from x-amz-request-id - IPv6 handling in stripPort - Quiet element whitespace tolerance in XML parser - content-type: application/xml on empty 2xx responses - Duplicate interfaces/s3/ -> re-exports from utils/s3/ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,44 +1,10 @@
|
||||
export const S3_CORS_HEADERS: Record<string, string> = {
|
||||
'access-control-allow-origin': '*',
|
||||
'access-control-allow-methods': 'GET, PUT, HEAD, DELETE, POST, OPTIONS',
|
||||
'access-control-allow-headers': [
|
||||
'Authorization',
|
||||
'Content-Type',
|
||||
'Content-MD5',
|
||||
'Range',
|
||||
'If-Match',
|
||||
'If-None-Match',
|
||||
'If-Modified-Since',
|
||||
'If-Unmodified-Since',
|
||||
'X-Amz-*',
|
||||
'x-amz-*',
|
||||
].join(', '),
|
||||
'access-control-expose-headers': [
|
||||
'Accept-Ranges',
|
||||
'Content-Length',
|
||||
'Content-Range',
|
||||
'Content-Type',
|
||||
'ETag',
|
||||
'Last-Modified',
|
||||
'x-amz-id-2',
|
||||
'x-amz-request-id',
|
||||
].join(', '),
|
||||
'access-control-max-age': '86400',
|
||||
};
|
||||
|
||||
export const s3Headers = (
|
||||
requestId: string,
|
||||
extraHeaders: Record<string, string> = {},
|
||||
): Record<string, string> => ({
|
||||
...S3_CORS_HEADERS,
|
||||
...(requestId ? { 'x-amz-request-id': requestId, 'x-amz-id-2': requestId } : {}),
|
||||
...extraHeaders,
|
||||
});
|
||||
|
||||
export const applyS3Headers = (headers: Headers, requestId: string): Headers => {
|
||||
const result = new Headers(headers);
|
||||
for (const [key, value] of Object.entries(s3Headers(requestId))) {
|
||||
result.set(key, value);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
/**
|
||||
* Re-export from the canonical headers implementation.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
export {
|
||||
applyS3Headers,
|
||||
S3_CORS_HEADERS,
|
||||
s3Headers,
|
||||
} from '../../utils/s3/headers';
|
||||
|
||||
Reference in New Issue
Block a user