Skip to content

Commit 0f7b2cd

Browse files
authored
fix: use RFC3689 encoding for S3 signature v4 (#793)
1 parent fbc937c commit 0f7b2cd

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

src/storage/protocols/s3/signature-v4.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,11 +412,27 @@ export class SignatureV4 {
412412
return `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeadersString}\n${payloadHash}`
413413
}
414414

415+
/**
416+
* Encodes a URI component according to RFC 3986, as required by AWS Signature V4.
417+
* This differs from encodeURIComponent which doesn't encode certain characters
418+
* like parentheses that AWS requires to be percent-encoded.
419+
*/
420+
protected encodeRFC3986URIComponent(str: string): string {
421+
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
422+
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
423+
})
424+
}
425+
415426
protected constructCanonicalQueryString(query: Record<string, string>) {
416427
return Object.keys(query)
417428
.filter((key) => !(key in ALWAYS_UNSIGNABLE_QUERY_PARAMS))
418429
.sort()
419-
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(query[key] as string)}`)
430+
.map(
431+
(key) =>
432+
`${this.encodeRFC3986URIComponent(key)}=${this.encodeRFC3986URIComponent(
433+
query[key] as string
434+
)}`
435+
)
420436
.join('&')
421437
}
422438

src/test/s3-protocol.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,47 @@ describe('S3 Protocol', () => {
292292
expect(resp.Contents?.length).toBe(3)
293293
})
294294

295+
it('list objects with aws specific uri encoding', async () => {
296+
const bucket = await createBucket(client)
297+
const testObject1 = 'test (1).jpg'
298+
const testObject2 = `prefix-1/test-1!'(123)*.jpg`
299+
300+
await Promise.all([testObject1, testObject2].map((v) => uploadFile(client, bucket, v, 1)))
301+
302+
const resp = await client.send(
303+
new ListObjectsV2Command({
304+
Bucket: bucket,
305+
})
306+
)
307+
expect(resp.Contents?.length).toBe(2)
308+
309+
const resp2 = await client.send(
310+
new ListObjectsV2Command({
311+
Bucket: bucket,
312+
Prefix: 'test (',
313+
})
314+
)
315+
expect(resp2.Contents?.length).toBe(1)
316+
expect(resp2.Contents).toEqual([
317+
expect.objectContaining({
318+
Key: testObject1,
319+
}),
320+
])
321+
322+
const resp3 = await client.send(
323+
new ListObjectsV2Command({
324+
Bucket: bucket,
325+
Prefix: `prefix-1/test-1!'(123)*`,
326+
})
327+
)
328+
expect(resp3.Contents?.length).toBe(1)
329+
expect(resp3.Contents).toEqual([
330+
expect.objectContaining({
331+
Key: testObject2,
332+
}),
333+
])
334+
})
335+
295336
it('list keys and common prefixes', async () => {
296337
const bucket = await createBucket(client)
297338
const listBuckets = new ListObjectsV2Command({

0 commit comments

Comments
 (0)