Skip to content

Commit 672192e

Browse files
fix(standard-server): invalid content-disposition with non-ASCII filenames (#1500)
Fixes #1498 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved handling of special characters and international (non-ASCII) characters in filenames during content disposition, ensuring proper escaping and RFC 5987 encoding compatibility. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent cb6363b commit 672192e

File tree

2 files changed

+18
-6
lines changed

2 files changed

+18
-6
lines changed

packages/standard-server/src/utils.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,22 @@ beforeEach(() => {
88
vi.clearAllMocks()
99
})
1010

11-
it('generateContentDisposition', () => {
12-
expect(generateContentDisposition('')).toEqual('inline; filename=""; filename*=utf-8\'\'')
13-
expect(generateContentDisposition('test.txt')).toEqual('inline; filename="test.txt"; filename*=utf-8\'\'test.txt')
14-
expect(generateContentDisposition('!@#$%^%^&*()\'".txt')).toEqual('inline; filename="!@#$%^%^&*()\'\\".txt"; filename*=utf-8\'\'!%40%23%24%25^%25^%26%2A%28%29%27%22.txt')
11+
describe('generateContentDisposition', () => {
12+
it('handle normal filename', () => {
13+
expect(generateContentDisposition('test.txt')).toEqual('inline; filename="test.txt"; filename*=utf-8\'\'test.txt')
14+
})
15+
16+
it('handle empty filename', () => {
17+
expect(generateContentDisposition('')).toEqual('inline; filename=""; filename*=utf-8\'\'')
18+
})
19+
20+
it('escape " special char', () => {
21+
expect(generateContentDisposition('!@#$%^%^&*()\'".txt')).toEqual('inline; filename="!@#$%^%^&*()\'\\".txt"; filename*=utf-8\'\'!%40%23%24%25^%25^%26%2A%28%29%27%22.txt')
22+
})
23+
24+
it('escape non-ASCII filenames', () => {
25+
expect(generateContentDisposition('テンプレ\'"ート.txt')).toEqual('inline; filename="____\'\\"__.txt"; filename*=utf-8\'\'%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%27%22%E3%83%BC%E3%83%88.txt')
26+
})
1527
})
1628

1729
it('getFilenameFromContentDisposition', () => {

packages/standard-server/src/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import type { StandardHeaders, StandardLazyResponse } from './types'
22
import { isAsyncIteratorObject, once, replicateAsyncIterator, toArray, tryDecodeURIComponent } from '@orpc/shared'
33

44
export function generateContentDisposition(filename: string): string {
5-
const escapedFileName = filename.replace(/"/g, '\\"')
5+
const encodedFileName = filename.replace(/[^\x20-\x7E]/g, '_').replace(/"/g, '\\"')
66

77
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_content-disposition_and_link_headers
88
const encodedFilenameStar = encodeURIComponent(filename)
99
.replace(/['()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
1010
.replace(/%(7C|60|5E)/g, (str, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
1111

12-
return `inline; filename="${escapedFileName}"; filename*=utf-8\'\'${encodedFilenameStar}`
12+
return `inline; filename="${encodedFileName}"; filename*=utf-8\'\'${encodedFilenameStar}`
1313
}
1414

1515
export function getFilenameFromContentDisposition(contentDisposition: string): string | undefined {

0 commit comments

Comments
 (0)