Fix/usr fdbck ch (#1105)
* fix(privacy): mask voucher amounts in session replays * fix: persist transaction source filter * fix: clarify invoice filenames and booking previews * fix: truncate long uploaded filenames * feat: add invoice delivery history * fix: harden invoice delivery history * fix: include invoice deliveries in full archive
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { contentDisposition } from '../content-disposition'
|
||||
import { contentDisposition, contentDispositionFilename } from '../content-disposition'
|
||||
|
||||
describe('contentDisposition', () => {
|
||||
it('passes a plain ASCII filename through unchanged in both forms', () => {
|
||||
@@ -45,12 +45,13 @@ describe('contentDisposition', () => {
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('neutralizes quote and CRLF header injection', () => {
|
||||
const header = contentDisposition('attachment', 'evil"\r\nSet-Cookie: x=y.pdf')
|
||||
it('neutralizes header delimiters and CRLF injection', () => {
|
||||
const header = contentDisposition('attachment', 'evil";\\\r\nSet-Cookie: x=y.pdf')
|
||||
expect(header).not.toContain('\r')
|
||||
expect(header).not.toContain('\n')
|
||||
expect(header).toContain('filename="evil___Set-Cookie: x=y.pdf"')
|
||||
expect(header).toContain('filename="evil_____Set-Cookie: x=y.pdf"')
|
||||
// The extended form percent-encodes them instead of emitting them raw.
|
||||
expect(header).toContain('%22%3B%5C')
|
||||
expect(header).toContain('%0D%0A')
|
||||
})
|
||||
|
||||
@@ -89,3 +90,25 @@ describe('contentDisposition', () => {
|
||||
expect(() => new Headers({ 'Content-Disposition': header })).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('contentDispositionFilename', () => {
|
||||
it('prefers and decodes the UTF-8 filename', () => {
|
||||
const header = contentDisposition(
|
||||
'attachment',
|
||||
'Företag x Kund AB Faktura nr 2621 20260721.pdf',
|
||||
)
|
||||
|
||||
expect(contentDispositionFilename(header))
|
||||
.toBe('Företag x Kund AB Faktura nr 2621 20260721.pdf')
|
||||
})
|
||||
|
||||
it('falls back to the quoted ASCII filename', () => {
|
||||
expect(contentDispositionFilename('attachment; filename="faktura-2621.pdf"'))
|
||||
.toBe('faktura-2621.pdf')
|
||||
})
|
||||
|
||||
it('returns null for a missing or malformed filename', () => {
|
||||
expect(contentDispositionFilename(null)).toBeNull()
|
||||
expect(contentDispositionFilename('attachment')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,9 +28,9 @@ export function contentDisposition(
|
||||
const normalized = filename.toWellFormed().normalize('NFC')
|
||||
|
||||
// ASCII fallback for the quoted-string form: anything outside printable
|
||||
// ASCII, plus the quoted-string specials " and \, becomes _. This also
|
||||
// neutralizes CR/LF header injection.
|
||||
const fallback = normalized.replace(/[^\x20-\x7e]|["\\]/g, '_')
|
||||
// ASCII, plus structurally significant header characters, becomes _. This
|
||||
// also neutralizes CR/LF header injection.
|
||||
const fallback = normalized.replace(/[^\x20-\x7e]|["\\;]/g, '_')
|
||||
|
||||
// RFC 5987 value-chars: encodeURIComponent covers everything except
|
||||
// ! ' ( ) * which it leaves bare but RFC 5987 forbids unencoded.
|
||||
@@ -41,3 +41,19 @@ export function contentDisposition(
|
||||
|
||||
return `${type}; filename="${fallback}"; filename*=UTF-8''${encoded}`
|
||||
}
|
||||
|
||||
/** Read the preferred UTF-8 filename from a Content-Disposition header. */
|
||||
export function contentDispositionFilename(header: string | null): string | null {
|
||||
if (!header) return null
|
||||
|
||||
const extended = header.match(/(?:^|;)\s*filename\*=UTF-8''([^;]*)/i)
|
||||
if (extended?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(extended[1])
|
||||
} catch {
|
||||
// Fall through to the ASCII quoted-string form.
|
||||
}
|
||||
}
|
||||
|
||||
return header.match(/(?:^|;)\s*filename="([^"]*)"/i)?.[1] ?? null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user