Skip to content

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

Open
Wang-Yeah623 wants to merge 3 commits into
larksuite:mainfrom
Wang-Yeah623:fix/multipart-formfile-escape-quotes
Open

fix(common): escape special chars in multipart form filenames#1037
Wang-Yeah623 wants to merge 3 commits into
larksuite:mainfrom
Wang-Yeah623:fix/multipart-formfile-escape-quotes

Conversation

@Wang-Yeah623
Copy link
Copy Markdown

@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

    • Improved multipart form handling by relying on the standard writer and clarified filename escaping behavior in documentation to ensure filenames are correctly represented in multipart headers.
  • Tests

    • Added tests that validate filename escaping/round-tripping in multipart Content-Disposition and confirm 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57b1af9d-1f55-49df-915c-12b19474e79a

📥 Commits

Reviewing files that changed from the base of the PR and between 46974bf and 528022b.

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

📝 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

🐰 A helper trimmed, the stdlib now in play,
Filenames escape as they ought, no manual fray.
Tests hop in to check each case,
Quiet headers, tidy space.
Cheers — the writer passed the day!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: removing the custom CreateFormFile override to allow stdlib escaping of special characters in multipart form filenames.
Description check ✅ Passed The PR description is comprehensive and complete, covering all template sections: a detailed summary explaining the bug, clear list of changes, thorough test plan with execution results, and related issues noted.
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.
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.

2 participants