Skip to content

Commit 9b06abc

Browse files
authored
fix: validate header before returning them (#717)
1 parent 853d386 commit 9b06abc

File tree

2 files changed

+49
-2
lines changed

2 files changed

+49
-2
lines changed

src/storage/protocols/s3/s3-handler.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1239,14 +1239,35 @@ export class S3ProtocolHandler {
12391239
}
12401240
}
12411241

1242+
const MAX_HEADER_NAME_LENGTH = 1024 * 8 // 8KB, per RFC7230 §3.2.6
1243+
const MAX_HEADER_VALUE_LENGTH = 1024 * 8 // 8KB, per RFC7230 §3.2.4
1244+
1245+
// Allowed header‐name chars per RFC7230 §3.2.6 “token”
1246+
// (note that the backtick ` is escaped)
1247+
const HEADER_NAME_RE = /^[!#$%&'*+\-.\^_`|~0-9A-Za-z]+$/
1248+
1249+
// Allowed header‐value chars per RFC7230 §3.2.4
1250+
// (horizontal tab, space–tilde, and 0x80–0xFF)
1251+
const HEADER_VALUE_RE = /^[\t\x20-\x7e\x80-\xff]+$/
1252+
1253+
export function isValidHeader(name: string, value: string | string[]): boolean {
1254+
if (Buffer.from(name).byteLength < MAX_HEADER_NAME_LENGTH && !HEADER_NAME_RE.test(name)) {
1255+
return false
1256+
}
1257+
const values = Array.isArray(value) ? value : [value]
1258+
return values.every(
1259+
(v) => Buffer.from(v).byteLength <= MAX_HEADER_VALUE_LENGTH && HEADER_VALUE_RE.test(v)
1260+
)
1261+
}
1262+
12421263
function toAwsMeatadataHeaders(records: Record<string, any>) {
12431264
const metadataHeaders: Record<string, any> = {}
12441265
let missingCount = 0
12451266

12461267
if (records) {
12471268
Object.keys(records).forEach((key) => {
12481269
const value = records[key]
1249-
if (value && isUSASCII(value)) {
1270+
if (value && isUSASCII(value) && isValidHeader(key, value)) {
12501271
metadataHeaders['x-amz-meta-' + key.toLowerCase()] = value
12511272
} else {
12521273
missingCount++

src/test/s3-protocol.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,21 @@ async function createBucket(client: S3Client, name?: string, publicRead = true)
5555
return bucketName
5656
}
5757

58-
async function uploadFile(client: S3Client, bucketName: string, key: string, mb: number) {
58+
async function uploadFile(
59+
client: S3Client,
60+
bucketName: string,
61+
key: string,
62+
mb: number,
63+
headers?: Record<string, any>
64+
) {
5965
const uploader = new Upload({
6066
client: client,
6167
params: {
6268
Bucket: bucketName,
6369
Key: key,
6470
ContentType: 'image/jpg',
6571
Body: Buffer.alloc(1024 * mb),
72+
Metadata: headers,
6673
},
6774
})
6875

@@ -175,6 +182,7 @@ describe('S3 Protocol', () => {
175182
expect(resp.$metadata.httpStatusCode).toBe(200)
176183
expect(resp.BucketRegion).toBe(storageS3Region)
177184
})
185+
178186
it('will return bucket not found error', async () => {
179187
const headBucketRequest = new HeadBucketCommand({
180188
Bucket: 'dont-exist-bucket',
@@ -1399,6 +1407,24 @@ describe('S3 Protocol', () => {
13991407
expect(resp.status).toBe(400)
14001408
})
14011409

1410+
it('doesnt crash when invalid headers returned', async () => {
1411+
const bucket = await createBucket(client)
1412+
const key = 'test-1.jpg'
1413+
await uploadFile(client, bucket, key, 2, {
1414+
'invalid-header-r': Buffer.alloc(1024 * 9, 'a').toString(),
1415+
})
1416+
1417+
const headObj = new HeadObjectCommand({
1418+
Bucket: bucket,
1419+
Key: key,
1420+
})
1421+
1422+
const r = await client.send(headObj)
1423+
1424+
expect(r.$metadata.httpStatusCode).toBe(200)
1425+
expect(r.MissingMeta).toBe(1)
1426+
})
1427+
14021428
it('can upload with presigned URL', async () => {
14031429
const bucket = await createBucket(client)
14041430
const key = 'test-1.jpg'

0 commit comments

Comments
 (0)