Skip to content

Commit ff9670e

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

5 files changed

Lines changed: 103 additions & 1 deletion

File tree

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
@@ -2,6 +2,7 @@ import { generateHS512JWK, SignedToken, signJWT, verifyJWT } from '@internal/aut
22
import axios from 'axios'
33
import dotenv from 'dotenv'
44
import { FastifyInstance } from 'fastify'
5+
import { randomUUID } from 'node:crypto'
56
import fs from 'fs/promises'
67
import path from 'path'
78
import app from '../app'
@@ -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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ describe('S3Backend', () => {
116116
})
117117

118118
describe('uploadPartCopy', () => {
119-
test('should preserve "/" and encode path tokens in CopySource', async () => {
119+
test('should preserve "/" and encode path tokens in CopySource for unicode source keys', async () => {
120120
const lastModified = new Date('2024-01-01T00:00:00.000Z')
121121
mockSend.mockResolvedValue({
122122
CopyPartResult: {

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)