fix(common): escape special chars in multipart form filenames#1037
Conversation
MultipartWriter.CreateFormFile concatenated the fieldname and filename
into the Content-Disposition header without escaping, so a filename
containing a double-quote, backslash, CR, or LF produced a malformed
header. For example, uploading `report "draft" v2.pdf` via
`task +upload-attachment` made the server see `filename="report "`
(truncated at the first internal quote) and drop the rest.
Drop the custom override and let CreateFormFile be promoted from the
embedded *multipart.Writer, which applies the stdlib's quoteEscaper
(backslash and double-quote get a backslash prefix; CR and LF get
percent-encoded). The Content-Type ("application/octet-stream") and
the wrapper API are unchanged, so the existing `task +upload-attachment`
call site is unaffected -- filenames with special characters just now
round-trip correctly.
Add helpers_test.go covering plain, quoted, backslashed, mixed, and
unicode filenames. The test asserts both the on-wire encoding and a
round-trip through mime.ParseMediaType (bypassing Part.FileName, whose
filepath.Base is platform-dependent for backslash on Windows).
📝 WalkthroughWalkthroughThe custom CreateFormFile method was removed from MultipartWriter; the type now relies on the embedded *multipart.Writer's promoted CreateFormFile. Documentation was updated and two tests were added to validate filename escaping and part Content-Type. ChangesMultipart Writer Refactoring
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
shortcuts/common/helpers_test.go (1)
30-40: ⚡ Quick winAdd CR/LF filename cases to lock down the full escaping regression surface.
Line 30-40 covers quotes/backslashes/unicode, but not
\r/\n, which are part of the malformed-header class this PR addresses. Adding explicit CR/LF cases would make the regression suite complete.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@shortcuts/common/helpers_test.go` around lines 30 - 40, Add explicit CR/LF test cases to the existing cases table in helpers_test.go: extend the cases slice (the same variable shown) with entries for CR, LF, and CRLF filenames such as {"with CR", "file\rname.pdf", "file\\rname.pdf"}, {"with LF", "file\nname.pdf", "file\\nname.pdf"}, and {"with CRLF", "file\r\nname.pdf", "file\\r\\nname.pdf"} so the test asserts the header-escaping logic handles carriage return and newline correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@shortcuts/common/helpers_test.go`:
- Around line 30-40: Add explicit CR/LF test cases to the existing cases table
in helpers_test.go: extend the cases slice (the same variable shown) with
entries for CR, LF, and CRLF filenames such as {"with CR", "file\rname.pdf",
"file\\rname.pdf"}, {"with LF", "file\nname.pdf", "file\\nname.pdf"}, and {"with
CRLF", "file\r\nname.pdf", "file\\r\\nname.pdf"} so the test asserts the
header-escaping logic handles carriage return and newline correctly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fd29b4f4-1c05-4ca3-990f-9cb56934959c
📒 Files selected for processing (2)
shortcuts/common/helpers.goshortcuts/common/helpers_test.go
Per code-review feedback, extend the helpers_test.go cases table with
CR, LF, and CRLF filenames so the test exercises both legs of the
stdlib's quoteEscaper:
- backslash and double-quote use backslash escaping (quoted-pair);
these round-trip exactly through mime.ParseMediaType.
- CR and LF use percent encoding to prevent header injection; the
MIME parser does not decode percent escapes, so the read-side
filename param contains literal "%0D"/"%0A".
The cases table grows a wantParsed column so each case can declare its
expected post-parse value (same as filename for backslash-escaped chars,
percent-encoded for CR/LF).
Two follow-up tweaks suggested by a re-read of the PR:
- helpers.go: stop naming the stdlib's internal `quoteEscaper` in the
doc comment. Describe the observable behaviour ("escapes special
characters") instead, so the comment stays valid if the stdlib ever
renames or reimplements its escaping.
- helpers_test.go: rename the vague `with both` case to
`backslash and quote`; split the table-driven cases into three
visually-separated groups (happy path / backslash escaping /
percent encoding) so it is obvious why two cases have a different
wantParsed than filename.
No behaviour change; tests still pass 8/8.
🚀 PR Preview Install Guide🧰 CLI updatenpm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@163703fc3e22026c24f9e3884aff46eb4ef1eea5🧩 Skill updatenpx skills add Wang-Yeah623/cli#fix/multipart-formfile-escape-quotes -y -g |
CI runs against the toolchain pinned in go.mod (1.23.0), whose multipart/Writer.quoteEscaper escapes only backslash and double-quote. Percent-encoding of CR and LF was added to the stdlib later, so the three CR / LF / CRLF cases I added on review feedback fail on CI: the literal CR/LF lands in the Content-Disposition header and the parser reports `malformed MIME header: missing colon`. Drop those three cases. The fix in the prior commits still covers the real-world bug — backslash and double-quote in filenames — which is what the original `report "draft".pdf` example demonstrates. CR or LF in a filename is essentially never legal on any supported OS, so leaving that edge case to a future stdlib upgrade keeps the test stable across toolchains. Also dropped the now-unused wantParsed column from the cases table: with only round-trippable characters left, mime.ParseMediaType returns the original filename byte-for-byte, so a single tc.filename comparison suffices.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
shortcuts/common/helpers_test.go (1)
51-88: 💤 Low valueConsider adding
t.Parallel()inside each subtest for better parallelization.The parent test has
t.Parallel()at line 34, but each subtest could also be marked parallel to allow them to run concurrently with each other. This would improve test execution time.⚡ Optional optimization
for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + t.Parallel() var buf bytes.Buffer mw := NewMultipartWriter(&buf)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@shortcuts/common/helpers_test.go` around lines 51 - 88, Add t.Parallel() at the start of each subtest goroutine so the t.Run anonymous function runs concurrently with other subtests: inside the loop where you call t.Run(tc.name, func(t *testing.T) { ... }), insert t.Parallel() as the first statement of that anonymous test function (the scope that calls NewMultipartWriter, mw.CreateFormFile, mime.ParseMediaType, etc.) to enable parallel execution of the subtests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@shortcuts/common/helpers_test.go`:
- Around line 51-88: Add t.Parallel() at the start of each subtest goroutine so
the t.Run anonymous function runs concurrently with other subtests: inside the
loop where you call t.Run(tc.name, func(t *testing.T) { ... }), insert
t.Parallel() as the first statement of that anonymous test function (the scope
that calls NewMultipartWriter, mw.CreateFormFile, mime.ParseMediaType, etc.) to
enable parallel execution of the subtests.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9a208910-1ade-403c-840c-25bb60fd2bf6
📒 Files selected for processing (1)
shortcuts/common/helpers_test.go
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1037 +/- ##
==========================================
+ Coverage 67.79% 67.84% +0.05%
==========================================
Files 591 592 +1
Lines 55238 55322 +84
==========================================
+ Hits 37449 37534 +85
+ Misses 14680 14676 -4
- Partials 3109 3112 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary
MultipartWriter.CreateFormFileconcatenated the fieldname and filename into theContent-Dispositionheader without escaping, so any filename containing",\, CR, or LF produced a malformed header. The only consumer today istask +upload-attachment, which receives the filename fromfilepath.Baseon the user's path — so e.g. uploadingreport "draft" v2.pdfon macOS/Linux makes the server seefilename="report "and drop the rest.Changes
CreateFormFileoverride inshortcuts/common/helpers.go. The wrapper still embeds*multipart.Writer, somw.CreateFormFilenow resolves to the stdlib version, which appliesquoteEscaper(backslash →\\, double-quote →\", CR →%0D, LF →%0A). Behavior is otherwise identical (sameapplication/octet-streamContent-Type, same signature).shortcuts/common/helpers_test.gocovering plain, quoted, backslashed, mixed-special, and unicode filenames. The tests both inspect the on-wire bytes for the escaped form and round-trip the header throughmime.ParseMediaTypeto confirm the server-side filename matches the input.Test Plan
go test ./shortcuts/common/ -run TestMultipartWriter— passes (5/5 sub-cases + Content-Type check)main:with_double_quoteandwith_bothblow up withmime: invalid media parameter;with_backslashfails the on-wire byte assertiongo test ./shortcuts/task/— passes (only consumer ofMultipartWriter)go vet ./...,gofmt -l,go build ./...— all cleanlark-cli task +upload-attachmentwith a filename containing"(covered by unit tests; live run needs bot creds)Related Issues
Summary by CodeRabbit
Refactor
Tests