Skip to content

fix: encode download Content-Disposition per RFC 6266 and RFC 8187 - #1386

Merged
ferhatelmas merged 2 commits into
supabase:masterfrom
hsusul:fix/content-disposition-rfc8187
Sep 22, 2026
Merged

ferhatelmas merged 2 commits into
supabase:masterfrom
hsusul:fix/content-disposition-rfc8187

Conversation

@hsusul

@hsusul hsusul commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

What

Encode the ?download= Content-Disposition header per RFC 6266 / RFC 8187.

Fixes #1385. Follows up #735, which fixed the trailing semicolon in the same header.

Why

Renderer.handleDownload used encodeURIComponent output for both parameters:

  • filename* — encodeURIComponent leaves ' ( ) * raw, but an RFC 8187 ext-value only allows attr-char unencoded, and ' is its delimiter. John's Resume.pdf → UTF-8''John's%20Resume.pdf is malformed.
  • filename — the fallback for clients that ignore filename* is not percent-decoded by anyone, so my file.pdf was saved as my%20file.pdf, and report(1).pdf was sent as an unquoted token containing delimiters.

This is the only place download= becomes a header, so every public, signed and authenticated object and image route is covered.

How

  • filename*: percent-encode ' ( ) * on top of encodeURIComponent, giving a value made of attr-char and %XX only.
  • filename: printable ASCII fallback — non-ASCII and control characters (so CR/LF can never reach the header), plus " and \ (RFC 6266 Appendix D), become _. It is quoted only when it isn't a valid token, so plain names like testname.png keep today's exact header (the existing assertion in src/test/object.test.ts is unchanged).
download= before after
report.pdf filename=report.pdf; filename*=UTF-8''report.pdf unchanged
my file.pdf filename=my%20file.pdf; filename*=UTF-8''my%20file.pdf filename="my file.pdf"; filename*=UTF-8''my%20file.pdf
John's Resume.pdf …; filename*=UTF-8''John's%20Resume.pdf filename="John's Resume.pdf"; filename*=UTF-8''John%27s%20Resume.pdf
naïve.txt filename=na%C3%AFve.txt; … filename=na_ve.txt; filename*=UTF-8''na%C3%AFve.txt

Tests

New src/storage/renderer/renderer.test.ts parses the header with the RFC 6266 grammar (token or quoted-string filename) and checks that filename* contains only attr-char/%XX, round-trips the name, and that the fallback is correct. It also covers: no download, empty download, an already-valid token staying byte-identical, non-ASCII, and a CR/LF injection attempt.

6 of the 11 cases fail on master (spaces, apostrophe, parentheses, *, "/\, non-ASCII); the other 5 are guards for behavior that is already correct.

Validation

Local, macOS / Node v24.9.0:

Not run: npm run test:integration (needs the Docker Postgres stack; port 5432 is occupied on this machine). Locally, a few unrelated unit tests (s3/index, request-scoped-pg-executor, vector, listObjectsV2 migration gate) fail intermittently under load. They fail the same way on clean master in this environment and pass in upstream CI.

@hsusul
hsusul requested a review from a team as a code owner September 11, 2026 18:16
@coveralls

coveralls commented Sep 11, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 35739742579

Coverage increased (+0.004%) to 83.396%

Details

  • Coverage increased (+0.004%) from the base build.
  • Patch coverage: 5 of 5 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 14205
Covered Lines: 12274
Line Coverage: 86.41%
Relevant Branches: 8699
Covered Branches: 6827
Branch Coverage: 78.48%
Branches in Coverage %: Yes
Coverage Strength: 765.82 hits per line

💛 - Coveralls

hsusul and others added 2 commits September 22, 2026 15:48
The download= parameter was turned into both Content-Disposition
parameters with encodeURIComponent. That leaves ' ( ) * raw in the
filename* ext-value, where only attr-char is allowed and "'" is the
delimiter, so a name like "John's Resume.pdf" produced a malformed
value. It also percent-encoded the plain filename fallback, which
clients never decode, so "my file.pdf" was saved as "my%20file.pdf",
and names with parentheses produced an invalid unquoted token.

Percent-encode everything outside attr-char in filename*, and build the
filename fallback from printable ASCII instead: non-ASCII and control
characters (keeping CR/LF out of the header) and '"'/'\' become "_", as
RFC 6266 Appendix D advises, and the value is quoted only when it is not
a valid token, so plain names keep today's exact header.
Signed-off-by: Ferhat Elmas <elmas.ferhat@gmail.com>
@ferhatelmas
ferhatelmas force-pushed the fix/content-disposition-rfc8187 branch from 34f717b to 91eb75a Compare September 22, 2026 14:21
@ferhatelmas
ferhatelmas enabled auto-merge (squash) September 22, 2026 14:25
@ferhatelmas
ferhatelmas merged commit 0e7910f into supabase:master Sep 22, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

download= produces a Content-Disposition that corrupts filenames with apostrophes, spaces or parentheses

3 participants