Skip to content

fix(core): honor quoted-strings when parsing Content-Disposition filename - #118

Merged
dinwwwh merged 5 commits into
mainfrom
claude/stoic-edison-554dsb
Sep 29, 2026
Merged

dinwwwh merged 5 commits into
mainfrom
claude/stoic-edison-554dsb

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

getFilenameFromContentDisposition now reads parameters with a quote-aware scanner. It used to find filename*= and filename= with regexes that ignored quoted-strings, so a ;filename*=... inside the quoted fallback written by generateContentDisposition was taken as the real parameter. A File named invoice;filename*=utf-8''invoice.exe;.pdf sent through toFetchBody arrived as invoice.exe on the fetch, node, peer and aws-lambda receivers, and report;filename*=utf-8''evil.exe arrived as evil.exe".

Fixes

  • Filenames containing ;, =, ' or * now round-trip exactly through generateContentDisposition and getFilenameFromContentDisposition.
  • generateContentDisposition replaces ; and = with _ in the filename="..." fallback, so receivers still on the released parser can't be misled either. The exact name still travels in filename*.

Notes for reviewers

  • The fallback change only affects clients that ignore filename*, which already see _ for non-ASCII characters. The core README now says so.
  • A few unusual headers parse differently: a quoted filename*="utf-8''..." is accepted, whitespace around = is allowed, and an unquoted filename= value keeps any " it contains. An unterminated quoted-string now runs to the end of the header, so nothing after it is read as a parameter.
  • The scanner is a sticky regex exec loop and stays linear on hostile input: a 16 KB header parses in under 1 ms. All regexes are hoisted to module scope.

Testing

  • New unit tests for ; and parameters inside quoted values, unterminated quoted-strings, quoted filename*, whitespace and case, plus a round-trip test over tricky filenames.
  • Fuzzed 60k random filenames against the final code in three directions: new sender to new receiver, old sender to new receiver, and new sender to the released receiver. All came back exact.
  • Full suite, lint and type check pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_0119s8HvkwUjgBK9EGP6gRWs

…name

getFilenameFromContentDisposition matched `filename*=` / `filename=` with
quote-unaware regexes, so a `;filename*=...` inside the quoted legacy
filename was taken as the real parameter. A File named
"invoice;filename*=utf-8''invoice.exe;.pdf" arrived as "invoice.exe" on
fetch, node, peer and aws-lambda receivers.

Parse parameters with a quote-aware tokenizer (first occurrence wins,
unterminated quoted-strings run to the end), and drop ';' and '=' from
the generated fallback filename so receivers still on the old parser
cannot be misled either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119s8HvkwUjgBK9EGP6gRWs
Replace matchAll with a sticky regex exec loop and let a leading [\s;]*
absorb separator runs in one match. Typical headers parse in about half
the time, and ';'-flooded headers go from ~2ms to ~16us.

Also trim the comments and drop test cases that duplicated existing ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119s8HvkwUjgBK9EGP6gRWs
Move the remaining inline regex literals in core utils into named
module-level constants, matching the rest of the repo. The global ones are
only used with replace(), which resets lastIndex, and the others are
stateless, so sharing them is safe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119s8HvkwUjgBK9EGP6gRWs
@pkg-pr-new

pkg-pr-new Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
@standard-server/aws-lambda

npm i https://pkg.pr.new/@standard-server/aws-lambda@118

@standard-server/core

npm i https://pkg.pr.new/@standard-server/core@118

@standard-server/fastify

npm i https://pkg.pr.new/@standard-server/fastify@118

@standard-server/fetch

npm i https://pkg.pr.new/@standard-server/fetch@118

@standard-server/node

npm i https://pkg.pr.new/@standard-server/node@118

@standard-server/peer

npm i https://pkg.pr.new/@standard-server/peer@118

@standard-server/shared

npm i https://pkg.pr.new/@standard-server/shared@118

commit: a0d10a6

@codecov

codecov Bot commented Sep 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/stoic-edison-554dsb (a0d10a6) with main (f414f41)

Open in CodSpeed

Footnotes

  1. 108 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the parser rewrite is sound and well tested. One deliberate output change is worth confirming.

Reviewed changes — replaced the ad-hoc regex extraction in getFilenameFromContentDisposition with a single sticky-regex parameter scanner; initial review of all 4 commits.

  • New getContentDispositionParam() scanner (packages/core/src/utils.ts:30): extracts a named parameter while respecting quoted-string boundaries and escaped pairs, so a ;/filename*= embedded inside a quoted value can no longer be read as a real parameter. The lastIndex loop always advances (proved by fuzzing), so the non-null assertion is safe.
  • getFilenameFromContentDisposition() rewrite (:46): prefers filename* (charset/language handling unchanged) and otherwise falls back to the plain filename, both via the scanner. Closes the old injection where filename="report;filename*=utf-8''evil.exe" returned evil.exe.
  • generateContentDisposition() fallback escaping change (:4, :19): the ASCII filename="..." fallback now also replaces ; and = with _; filename* still preserves them.
  • Tests (utils.test.ts): injection, unterminated-quote, whitespace/case, quoted-filename*, and generator round-trip cases.

Verification: affected suites pass (400 tests across core/fetch/node/fastify/aws-lambda) and core tsc -b is clean. I fuzzed the scanner with 200k random strings (no hang, no zero-width match, no exec null), ran 20k exact round-trips (all clean except the documented lone-surrogate normalization), and confirmed no ReDoS on 200k-char adversarial inputs (all <7ms).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Comment thread packages/core/src/utils.ts
@dinwwwh dinwwwh changed the title Improve Content-Disposition header parsing robustness fix(core): honor quoted-strings when parsing Content-Disposition filename Sep 29, 2026
@dinwwwh
dinwwwh merged commit d6b4eff into main Sep 29, 2026
11 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.

2 participants