Skip to content

fix(common): escape special chars in multipart form filenames#1037

Merged
liangshuo-1 merged 5 commits into
larksuite:mainfrom
Wang-Yeah623:fix/multipart-formfile-escape-quotes
May 25, 2026
Merged

fix(common): escape special chars in multipart form filenames#1037
liangshuo-1 merged 5 commits into
larksuite:mainfrom
Wang-Yeah623:fix/multipart-formfile-escape-quotes

Conversation

@Wang-Yeah623
Copy link
Copy Markdown
Contributor

@Wang-Yeah623 Wang-Yeah623 commented May 22, 2026

Summary

MultipartWriter.CreateFormFile concatenated the fieldname and filename into the Content-Disposition header without escaping, so any filename containing ", \, CR, or LF produced a malformed header. The only consumer today is task +upload-attachment, which receives the filename from filepath.Base on the user's path — so e.g. uploading report "draft" v2.pdf on macOS/Linux makes the server see filename="report " and drop the rest.

Changes

  • Drop the custom CreateFormFile override in shortcuts/common/helpers.go. The wrapper still embeds *multipart.Writer, so mw.CreateFormFile now resolves to the stdlib version, which applies quoteEscaper (backslash → \\, double-quote → \", CR → %0D, LF → %0A). Behavior is otherwise identical (same application/octet-stream Content-Type, same signature).
  • Add shortcuts/common/helpers_test.go covering 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 through mime.ParseMediaType to confirm the server-side filename matches the input.

Test Plan

  • go test ./shortcuts/common/ -run TestMultipartWriter — passes (5/5 sub-cases + Content-Type check)
  • Confirmed the new tests fail on main: with_double_quote and with_both blow up with mime: invalid media parameter; with_backslash fails the on-wire byte assertion
  • go test ./shortcuts/task/ — passes (only consumer of MultipartWriter)
  • go vet ./..., gofmt -l, go build ./... — all clean
  • Manual: lark-cli task +upload-attachment with a filename containing " (covered by unit tests; live run needs bot creds)

Related Issues

  • None

Summary by CodeRabbit

  • Refactor

    • Rely on the embedded standard multipart writer and clarify filename-escaping behavior in docs so filenames are correctly represented in multipart headers.
  • Tests

    • Added tests verifying filename escaping/round-tripping in Content-Disposition and confirming file parts use the expected content type.

Review Change Stack

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).
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 22, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

The 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.

Changes

Multipart Writer Refactoring

Layer / File(s) Summary
Refactor MultipartWriter to promoted CreateFormFile
shortcuts/common/helpers.go
Custom CreateFormFile method deleted; net/textproto import removed; documentation added explaining the method is promoted from the embedded *multipart.Writer to ensure correct escaping of Content-Disposition filenames.
Add regression tests for CreateFormFile behavior
shortcuts/common/helpers_test.go
Two tests validate that the promoted CreateFormFile correctly escapes special characters in filenames and that the created part uses application/octet-stream as the Content-Type.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nudged a helper, light and spry,
Let stdlib take the reins and fly.
Filenames safe, each quote and slash,
Tests hop in with a tiny bash.
Quiet burrow, headers pass.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: removing a custom CreateFormFile override and using the stdlib version that properly escapes special characters in multipart form filenames.
Description check ✅ Passed The description follows the required template structure with all major sections complete: Summary explains the bug, Changes lists the specific modifications with justification, Test Plan documents comprehensive testing with pass/fail markers, and Related Issues is addressed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the size/M Single-domain feat or fix with limited business impact label May 22, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
shortcuts/common/helpers_test.go (1)

30-40: ⚡ Quick win

Add 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

📥 Commits

Reviewing files that changed from the base of the PR and between ffcf778 and 078ef81.

📒 Files selected for processing (2)
  • shortcuts/common/helpers.go
  • shortcuts/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.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 25, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@163703fc3e22026c24f9e3884aff46eb4ef1eea5

🧩 Skill update

npx 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.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
shortcuts/common/helpers_test.go (1)

51-88: 💤 Low value

Consider 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

📥 Commits

Reviewing files that changed from the base of the PR and between 528022b and 163703f.

📒 Files selected for processing (1)
  • shortcuts/common/helpers_test.go

@codecov
Copy link
Copy Markdown

codecov Bot commented May 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.84%. Comparing base (9d4233b) to head (163703f).
⚠️ Report is 6 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@liangshuo-1 liangshuo-1 merged commit 8bc4ec3 into larksuite:main May 25, 2026
18 checks passed
@liangshuo-1 liangshuo-1 mentioned this pull request May 26, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants