potiuk opened a new pull request, #69:
URL: https://github.com/apache/comdev/pull/69
## Summary
`get_email` lists an email's attachments, but nothing could fetch them:
`get_source` and `get_mbox` truncate their output, so a base64-encoded
attachment was always cut off.
Security reports in particular ship their proof of concept as an attachment,
often as a password-protected ZIP with the password in the message text.
This adds `save_attachments`, which downloads an email's attachments through
PonyMail's attachment endpoint (`/api/email` with `attachment=true` and the
attachment hash) and writes them to disk.
The bytes never go into the tool result, only names, sizes, types and
SHA-256 digests.
With `password`, ZIP attachments (ZipCrypto or AES) are decrypted and
extracted next to the archive.
### Where it may write
- Disabled unless `PONYMAIL_ATTACHMENT_DIRS` lists the directories the
server may write into.
The roots are created and pinned (canonical path, device, inode) at
startup; a root later replaced or re-pointed makes the tool refuse.
- At startup a root is refused if it is a symlink, not owned by the server's
user, or writable by group or others, or if any component of the path (as
configured, symlinks included, and as resolved) is owned by another user or is
a group/other-writable directory that is not sticky. A directory or link
someone else planted, e.g. under `/tmp`, is never trusted.
- `dir` must be local, absolute, without `.`/`..` segments, and resolve
inside a pinned root; it is created at the canonical path the check approved.
UNC and device paths are refused before any filesystem lookup.
- Node has no `openat()`, so containment is checked on paths, before and
after creating the directory and again after opening each file, before anything
is written.
Those checks are defence in depth: the README requires the roots to be
**read-only to the caller** (for Claude Code, an `Edit(<dir>/**)` deny rule,
which also stops sandboxed commands writing or renaming there), so nothing can
race them.
- Filenames are reduced to ASCII `[A-Za-z0-9._-]`, with no separators, no
`..`, no leading dot, and a leading `_` on Windows device names (`CON`,
`NUL.txt`, `COM1`); files are created `0600` with `O_EXCL`, so nothing is
overwritten or written through a symlink.
### Downloads and archives
- The body is streamed and cancelled once it passes the reported size (50 MB
when none is reported), then checked against the reported size and SHA-256
(PonyMail's attachment hash is the content digest). An unencoded response whose
Content-Length is over the limit is refused before reading.
- Extraction reads at most 1000 entries from the central directory and
inflates at most 200 MB per archive, failed entries included; an entry is cut
off once it inflates past its declared size; CRC-32 is checked.
- Anything that can refuse the whole archive runs before the first write:
escaping entry names (zip.js refuses them) and the password, proven by
decrypting the first encrypted entry in full.
A later failure is reported together with the files already extracted.
- Symlink and special entries are never created; sanitized names that
collide with a directory are renumbered; the result caps how many entries it
lists and how long names are.
- 7z, RAR and PGP files are detected and saved, not decrypted.
- The password is never echoed back.
This adds one dependency, `@zip.js/zip.js` 2.23.0 (BSD-3-Clause, no
dependencies of its own); `npm run licenses` passes.
`get_email` also now reads Foal's list-shaped `attachments` field correctly
(it iterated it as an object, so hashes never showed) and prints each
attachment's hash.
Version bumped to 1.3.0 for the new tool.
## Test plan
- [x] `npm test`: 161 tests pass, 67 of them new (`attachments.test.js`,
`archives.test.js`): traversal, non-ASCII and Windows device names, roots and
symlink escapes, planted and group-writable roots or ancestors, directory swaps
before/after open and during resolution, root replacement after pinning, UNC
paths, streamed size limits, ZipCrypto and AES with right, wrong and missing
passwords, CRC failures, bombs with forged sizes, entry-count and budget
limits, name collisions.
- [x] Mutation checks: disabling the write-time containment check, the
canonical-path `mkdir`, the configured-path ownership check, CRC checking, the
budget charge for failed entries, or reverting the mode check each fails its
test.
- [x] roborev (codex), general and security review on every revision until
both passed with no findings on the final commit.
- [x] End to end over stdio against `lists.apache.org`: saved a public
dev@httpd patch, byte-identical, `0600`/`0700`; refused a disabled tool,
outside and lookalike-sibling dirs, relative and `..` paths, unknown hashes.
- [x] End to end against a local PonyMail stand-in serving an AES-encrypted
ZIP: no password → flagged; wrong password → reported, archive kept; right
password → extracted; 150 long entry names → result capped at ~19 KB.
- [x] `prek run --all-files` and the pre-push MCP test hook pass.
Context: [apache/magpie#1768](https://github.com/apache/magpie/issues/1768).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]