security: download-path confinement, CRLF rejection, resource limits - #210
security: download-path confinement, CRLF rejection, resource limits#210lucagattoni wants to merge 4 commits into
Conversation
… limits (code only) Code-only slice of PR Wh1isper#201, rebased onto current main (post-Wh1isper#204/Wh1isper#205). - download_attachment confines save_path within attachment_download_dir (default ~/Downloads); paths escaping it (incl. .. or symlinks) are rejected - reject CR/LF in recipients, header values, and attachment paths (SMTP-envelope injection, notably via bcc) - resource-exhaustion ceilings on page_size, email_ids count, attachment size Rebase notes: adopt upstream's _decoded_payload in the extracted _find_attachment_in_message helper; type _reject_crlf as Iterable[str | None] for the pyright check added in Wh1isper#204. Docs are re-homed separately. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013pd8meRq4jSiqaLq715nSD
download_attachment now confines save paths to attachment_download_dir (default ~/Downloads). The GreenMail E2E downloads into pytest's tmp_path, so set MCP_EMAIL_SERVER_ATTACHMENT_DOWNLOAD_DIR to that dir or the roundtrip download is rejected. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013pd8meRq4jSiqaLq715nSD
…r#205 structure) - security.md: rewrite 'Attachment access' for the new save-path confinement; sharpen 'HTTP transport security' (Host/Origin != auth); add 'Message and input limits' (CRLF rejection, page/id/attachment ceilings) - configuration.md: attachment_download_dir setting + env rows - transports.md: unauthenticated-gateway warning in 'DNS rebinding protection' - tools.md / troubleshooting.md: correct now-stale download_attachment 'resolves against working directory' guidance Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013pd8meRq4jSiqaLq715nSD
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #210 +/- ##
=======================================
+ Coverage 92.5% 92.7% +0.1%
=======================================
Files 10 10
Lines 2013 2074 +61
Branches 310 320 +10
=======================================
+ Hits 1863 1923 +60
- Misses 107 109 +2
+ Partials 43 42 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Close the codecov patch gap opened by extracting _find_attachment_in_message: unit-test the helper (found / not-found / no-filename / non-multipart) and the download_attachment attachment-size rejection, which were otherwise exercised only by the (uncounted) GreenMail E2E. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013pd8meRq4jSiqaLq715nSD
|
Thank you for putting together this security hardening work and for documenting the threat model so clearly. Since this PR was opened, #212 has landed a substantial architecture rewrite that addresses these concerns through the new application boundaries:
The attachment path contract was also intentionally resolved differently from this PR: compatibility preserves the caller's exact destination rather than confining it under a configured download root, with the filesystem defenses above providing the security boundary. As a result, the original issues are either covered by #212 or superseded by the accepted exact-destination design, and this branch now conflicts with the rewritten architecture. I’m closing this PR as superseded. Thank you again for the careful contribution; it helped highlight the boundaries that needed explicit treatment. |
What this delivers
Three security-hardening fixes to the email tools, rebased cleanly onto current
mainand with docs harmonized into the new multi-filedocs/structure (post-#205).This supersedes #201, which was branched a week ago and now conflicts with the docs reorganization (#205) and the pyright/tooling changes (#204). Same fixes, cleanly re-homed.
Fixes
download_attachmentpath confinement — save paths are confined toattachment_download_dir(default~/Downloads). Asave_pathescaping it via..or a symlink (including a final-component symlink) is rejected, so attacker-controlled attachment bytes can't be written over~/.ssh/authorized_keys, a crontab, or the config file. Newattachment_download_dirsetting +MCP_EMAIL_SERVER_ATTACHMENT_DOWNLOAD_DIR.bcc(handed to aiosmtplib without being placed in a header).page_size ≤ 500,≤ 100 email_idsper call, and a 25 MB attachment cap guard against self-DoS from unbounded tool inputs.Rebase / integration notes
_decoded_payloadin the extracted_find_attachment_in_messagehelper (upstream added that decoder after this branch was first written)._reject_crlfasIterable[str | None]to satisfy the pyright check added in chore: add pyright checks and update Python tooling #204.security.md(attachment access rewrite, HTTP-transport = not-auth, new "Message and input limits"),transports.md(unauthenticated-gateway warning under DNS-rebinding),configuration.md(setting + env rows), and corrected now-staledownload_attachmentguidance intools.md/troubleshooting.md.download_attachmentconfinement required pointingMCP_EMAIL_SERVER_ATTACHMENT_DOWNLOAD_DIRat the GreenMail test'stmp_path, or the roundtrip download is rejected.Test plan
uv run pytest -q— 578 passed (new:test_crlf_injection.py,test_download_path_confinement.py,test_resource_limits.py)uv run ruff check/ruff format --check— cleanuv run pyright— 0 errors (chore: add pyright checks and update Python tooling #204's check)mkdocs build --strict— builds, no broken anchorsprettier --check docs/*.md— cleanCompanion to #209 (permission model). The two are independent at the code level; when both land, whichever merges second needs a small
app.pyrebase where they both touchsend_email/save_to_mailbox.🤖 Generated with Claude Code
https://claude.ai/code/session_013pd8meRq4jSiqaLq715nSD