Skip to content

Commit bf8efff

Browse files
committed
fix: more test coverage
Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
1 parent 3da574d commit bf8efff

File tree

6 files changed

+108
-10
lines changed

6 files changed

+108
-10
lines changed

src/test/limits.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ describe('isValidKey', () => {
1717
expect(isValidKey('invalid\x01name')).toBe(false)
1818
})
1919

20+
test('accepts DEL (0x7F) as a valid key character', () => {
21+
expect(isValidKey('valid\x7Fname')).toBe(true)
22+
})
23+
2024
test('rejects non-characters U+FFFE and U+FFFF', () => {
2125
expect(isValidKey(`invalid${'\uFFFE'}`)).toBe(false)
2226
expect(isValidKey(`invalid${'\uFFFF'}`)).toBe(false)

src/test/object.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2431,6 +2431,34 @@ describe('testing retrieving signed URL', () => {
24312431
expect(body.error).toBe('InvalidSignature')
24322432
})
24332433

2434+
test('rejects double-encoded signed object paths', async () => {
2435+
const objectName = `public/signed-double-${randomUUID()}-일이삼.txt`
2436+
await seedObjectForRouteTest(objectName)
2437+
2438+
const urlToSign = `bucket2/${objectName}`
2439+
const jwtToken = await signJWT({ url: urlToSign }, jwtSecret, 100)
2440+
const encodedPath = urlToSign
2441+
.split('/')
2442+
.map((segment) => encodeURIComponent(segment))
2443+
.join('/')
2444+
2445+
const validResponse = await appInstance.inject({
2446+
method: 'GET',
2447+
url: `/object/sign/${encodedPath}?token=${jwtToken}`,
2448+
})
2449+
expect(validResponse.statusCode).toBe(200)
2450+
2451+
const doubleEncodedPath = encodedPath.replaceAll('%', '%25')
2452+
const doubleEncodedResponse = await appInstance.inject({
2453+
method: 'GET',
2454+
url: `/object/sign/${doubleEncodedPath}?token=${jwtToken}`,
2455+
})
2456+
2457+
expect(doubleEncodedResponse.statusCode).toBe(400)
2458+
const body = doubleEncodedResponse.json<{ error: string }>()
2459+
expect(body.error).toBe('InvalidSignature')
2460+
})
2461+
24342462
test('get object without a token', async () => {
24352463
const response = await appInstance.inject({
24362464
method: 'GET',

src/test/render-routes.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { randomUUID } from 'node:crypto'
12
import { generateHS512JWK, SignedToken, signJWT, verifyJWT } from '@internal/auth'
23
import axios from 'axios'
34
import dotenv from 'dotenv'
@@ -194,4 +195,53 @@ describe('image rendering routes', () => {
194195
const body = response.json<{ error: string }>()
195196
expect(body.error).toBe('InvalidSignature')
196197
})
198+
199+
it('will reject double-encoded signed render paths', async () => {
200+
const objectName = `authenticated/render-double-${randomUUID()}-일이삼.png`
201+
const encodedObjectName = objectName
202+
.split('/')
203+
.map((segment) => encodeURIComponent(segment))
204+
.join('/')
205+
206+
const uploadResponse = await appInstance.inject({
207+
method: 'POST',
208+
url: `/object/bucket2/${encodedObjectName}`,
209+
payload: Buffer.from('render double-encoded test'),
210+
headers: {
211+
authorization: `Bearer ${process.env.SERVICE_KEY}`,
212+
'content-type': 'image/png',
213+
'x-upsert': 'true',
214+
},
215+
})
216+
expect(uploadResponse.statusCode).toBe(200)
217+
218+
const signURLResponse = await appInstance.inject({
219+
method: 'POST',
220+
url: `/object/sign/bucket2/${encodedObjectName}`,
221+
payload: {
222+
expiresIn: 60000,
223+
transform: {
224+
width: 100,
225+
height: 100,
226+
resize: 'contain',
227+
},
228+
},
229+
headers: {
230+
authorization: `Bearer ${process.env.SERVICE_KEY}`,
231+
},
232+
})
233+
expect(signURLResponse.statusCode).toBe(200)
234+
235+
const signedURL = signURLResponse.json<{ signedURL: string }>().signedURL
236+
const signedURLParsed = new URL(signedURL, 'http://localhost')
237+
const doubleEncodedPath = signedURLParsed.pathname.replaceAll('%', '%25')
238+
const doubleEncodedResponse = await appInstance.inject({
239+
method: 'GET',
240+
url: `${doubleEncodedPath}${signedURLParsed.search}`,
241+
})
242+
243+
expect(doubleEncodedResponse.statusCode).toBe(400)
244+
const body = doubleEncodedResponse.json<{ error: string }>()
245+
expect(body.error).toBe('InvalidSignature')
246+
})
197247
})

src/test/s3-adapter.test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,14 @@ describe('S3Backend', () => {
107107
expect(mockSend).toHaveBeenCalledTimes(1)
108108
const command = mockSend.mock.calls[0][0] as CopyObjectCommand
109109
expect(command).toBeInstanceOf(CopyObjectCommand)
110-
expect(command.input.CopySource).toBe(
111-
encodeCopySourceByPathToken('test-bucket', sourceKey)
112-
)
110+
expect(command.input.CopySource).toBe(encodeCopySourceByPathToken('test-bucket', sourceKey))
113111
expect(command.input.CopySource).toContain('test-bucket/source%20path/')
114112
expect(command.input.CopySource).not.toContain('test-bucket%2F')
115113
})
116114
})
117115

118116
describe('uploadPartCopy', () => {
119-
test('should preserve "/" and encode path tokens in CopySource', async () => {
117+
test('should preserve "/" and encode path tokens in CopySource for unicode source keys', async () => {
120118
const lastModified = new Date('2024-01-01T00:00:00.000Z')
121119
mockSend.mockResolvedValue({
122120
CopyPartResult: {
@@ -147,9 +145,7 @@ describe('S3Backend', () => {
147145
expect(mockSend).toHaveBeenCalledTimes(1)
148146
const command = mockSend.mock.calls[0][0] as UploadPartCopyCommand
149147
expect(command).toBeInstanceOf(UploadPartCopyCommand)
150-
expect(command.input.CopySource).toBe(
151-
encodeCopySourceByPathToken('test-bucket', sourceKey)
152-
)
148+
expect(command.input.CopySource).toBe(encodeCopySourceByPathToken('test-bucket', sourceKey))
153149
expect(command.input.CopySource).toContain('test-bucket/source%20path/folder/')
154150
expect(command.input.CopySource).not.toContain('test-bucket%2F')
155151
expect(command.input.CopySourceRange).toBe('bytes=0-1024')

src/test/signed-url-route.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ describe('signed URL route path verification', () => {
1818
const signedObjectPath = 'bucket2/authenticated/casestudy.png'
1919
const rawPath = `/render/image/sign/${encodeObjectPathForURL(signedObjectPath)}?token=jwt`
2020

21-
expect(
22-
doesSignedTokenMatchRequestPath(rawPath, '/render/image/sign', signedObjectPath)
23-
).toBe(true)
21+
expect(doesSignedTokenMatchRequestPath(rawPath, '/render/image/sign', signedObjectPath)).toBe(
22+
true
23+
)
2424
})
2525

2626
test('rejects double-encoded request paths', () => {

src/test/webhook-filter.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,26 @@ describe('webhook filter', () => {
2828

2929
expect(disabled).toBe(false)
3030
})
31+
32+
test('does not match path-segment URL-encoded disableEvents entries from external config', () => {
33+
const objectName = '폴더/子目录/파일-🙂-q?foo=1&bar=%25+plus;semi:colon,#frag.png'
34+
const eventType = 'ObjectCreated:Post'
35+
const encodedByPathSegment = objectName
36+
.split('/')
37+
.map((segment) => encodeURIComponent(segment))
38+
.join('/')
39+
40+
const disabled = shouldDisableWebhookEvent(
41+
[`Webhook:${eventType}:bucket6/${encodedByPathSegment}`],
42+
eventType,
43+
{
44+
bucketId: 'bucket6',
45+
name: objectName,
46+
}
47+
)
48+
49+
expect(disabled).toBe(false)
50+
})
3151
})
3252

3353
function disabledEvents(eventType: string, objectName: string) {

0 commit comments

Comments
 (0)