fix: verify presigned S3 URLs against public host

This commit is contained in:
asepharyana
2026-07-07 04:21:01 +07:00
parent 85aa54592a
commit 5253fb384d
3 changed files with 194 additions and 81 deletions
+88 -7
View File
@@ -73,14 +73,95 @@ describe('S3 Auth (SigV4)', () => {
});
it('rejects malformed presigned URLs', async () => {
const result = await verifyPresignedUrl(
'http://localhost/bucket/key',
'GET',
'key',
'secret',
'us-east-1',
);
const result = await verifyPresignedUrl({
url: 'http://localhost/bucket/key',
method: 'GET',
headers: { host: 'localhost' },
s3AccessKey: 'key',
s3SecretKey: 'secret',
region: 'us-east-1',
});
expect(result.isValid).toBe(false);
expect(result.errorCode).toBe('AccessDenied');
});
const sha256hex = (data: string): string => {
const h = new Bun.CryptoHasher('sha256');
h.update(data);
return Array.from(h.digest())
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
};
const hmacSha256 = (key: Uint8Array, msg: string): Uint8Array => {
const h = new Bun.CryptoHasher('sha256', key);
h.update(msg);
return h.digest();
};
const signingKey = (secret: string, date: string, region: string): Uint8Array => {
const enc = (s: string) => new TextEncoder().encode(s);
let k = hmacSha256(enc(`AWS4${secret}`), date);
k = hmacSha256(k, region);
k = hmacSha256(k, 's3');
return hmacSha256(k, 'aws4_request');
};
const hex = (bytes: Uint8Array): string =>
Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
it('verifies presigned GET using the public request host', async () => {
const accessKey = 'teleuploader-admin';
const secret = 'unit-test-secret';
const host = 'upload.example.test';
const path = '/bucket/key.txt';
const amzDate = '20260707T120000Z';
const dateStamp = '20260707';
const sp = new URLSearchParams({
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
'X-Amz-Credential': `${accessKey}/${dateStamp}/us-east-1/s3/aws4_request`,
'X-Amz-Date': amzDate,
'X-Amz-Expires': '3600',
'X-Amz-SignedHeaders': 'host',
});
const canonicalQs = [...sp.entries()]
.sort(([aKey, aVal], [bKey, bVal]) => `${aKey}=${aVal}`.localeCompare(`${bKey}=${bVal}`))
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
const canonicalRequest = `GET\n${path}\n${canonicalQs}\nhost:${host}\n\nhost\nUNSIGNED-PAYLOAD`;
const hashedCanonical = sha256hex(canonicalRequest);
const scope = `${dateStamp}/us-east-1/s3/aws4_request`;
const stringToSign = `AWS4-HMAC-SHA256\n${amzDate}\n${scope}\n${hashedCanonical}`;
const sig = hex(hmacSha256(signingKey(secret, dateStamp, 'us-east-1'), stringToSign));
sp.set('X-Amz-Signature', sig);
const result = await verifyPresignedUrl({
url: `https://${host}${path}?${sp.toString()}`,
method: 'GET',
headers: { host },
s3AccessKey: accessKey,
s3SecretKey: secret,
region: 'us-east-1',
now: new Date('2026-07-07T12:05:00Z'),
});
expect(result.isValid).toBe(true);
});
it('rejects presigned URLs signed for a different host', async () => {
const result = await verifyPresignedUrl({
url: 'https://wrong.example.test/bucket/key.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=teleuploader-admin%2F20260707%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260707T120000Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=00',
method: 'GET',
headers: { host: 'upload.example.test' },
s3AccessKey: 'teleuploader-admin',
s3SecretKey: 'unit-test-secret',
region: 'us-east-1',
now: new Date('2026-07-07T12:05:00Z'),
});
expect(result.isValid).toBe(false);
expect(result.errorCode).toBe('SignatureDoesNotMatch');
});
});