Webmin 2.670, Users batch home is a raw prefix
Webmin 2.670 useradmin/batch_exec.cgi check_user compares home with a raw substr. save_user.cgi uses is_under_directory and appends a slash. ACL /home/a therefore matches /home/ab on batch create and is refused on the form. Labbed. No CVE yet.
- Name
- Webmin 2.670, Users batch home is a raw prefix
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- medium
- Disclosure Status
- public
- Vendor
- Webmin
- Affected
- Webmin 2.670 (3a2b2b86). Signed-in Users and Groups user with batch create, home ACL without a trailing slash, autohome off.
- Published
- 11 Nov 2026
- Updated
- 11 Nov 2026
- Tags
- 0-day, webmin, path, prefix, cwe-22, authenticated
/home/a is six characters. The form already knew that.
A Webmin help-desk account that is only allowed to put new home directories under /home/a can still create /home/ab through Users and Groups batch. The ordinary Create User form refuses that path. The ACL looks like a directory fence. Batch treats it as the first few characters of a string.
The proof of concept is on GitHub: abraxas/webmin-batch-home-prefix (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, entrypoint.sh, run.sh, poc.py. Authorized lab only. It talks to loopback.
This is Webmin 2.670 (3a2b2b8671c0669c129f5275b3dd1853106ae02b). No CVE yet. CWE-22. 6.5 Medium (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N). Remote path prefix. Authenticated Users and Groups batch create. Home ACL without a trailing slash.
This is not anonymous. This is not a shell module. Root already handed the account Users and Groups. The leftover is that batch create does not use the same fence as the form.
Why Webmin
Webmin is still the box people put on a Linux host so someone who is not root can make users, edit Apache, and restart services. Fine-grained ACLs are the product. webmin.com/security says so: do not file "I was given the shell module." File the case where the ACL denied the action and the CGI still did it.
I wanted that case. Unauth remote, or a signed-in user who was told no and still reached the sink. Default install of the tag people download. Master was 298 commits ahead the day I cloned. I did not hunt master.
Pinning 2.670
Latest published tag on 2026-11-04 was 2.670, 21 Sep 2026. Commit 3a2b2b8671c0669c129f5275b3dd1853106ae02b. Tree version says 2.670. I cloned --branch 2.670 --depth 1 and left it there.
The lab image is a source setup.sh of that tree on Debian bookworm, ssl off, login root / labroot, then an entrypoint that adds a limited Users and Groups user with home=/home/a. Not a distro package. Not Virtualmin.
The skip list is half the hunt
Already public on or before this pin, so not this post:
- custom-command parameter injection (fixed in 2.670)
- File Manager search / chooser directory listing (fixed in 2.670)
- DHCPd / MySQL / PostgreSQL / Nginx / Samba / Squid ACL bypass (2.653)
- URL-download SSRF (2.652)
- reflected XSS CVE-2026-49243 (2.650)
- status-module stored XSS CVE-2026-22678 (2.641)
- help-page root command (2.640)
- SVG mail XSS CVE-2026-49102
- attachment detach overwrite CVE-2026-49103
- Basic-auth 2FA bypass CVE-2026-42210 and CVE-2026-56022
- Squid cache-manager injection CVE-2025-67738
- password-reset host header CVE-2025-61541
- proxy client-cert spoof CVE-2026-56020
- shell autocomplete CVE-2024-12828
- File Manager CVE-2022-0824 and CVE-2022-0829
- arbitrary file write CVE-2022-30708
- backdoor CVE-2019-15231
I did not refile those. I did not treat default home=/ as a leftover. That ACL is the whole disk. The product said so.
Surfaces that returned nothing
I opened useradmin/save_user.cgi first because 2.670 had just taught File Manager about prefixes. Line 237 already calls is_under_directory. That helper lives in web-lib-funcs.pl. It refuses .., resolves links, then appends a slash and compares. /home/a becomes /home/a/. /home/ab is not under that.
Export CGIs use the same helper. gexport_exec.cgi, export_exec.cgi. Batch file path uses it against batchdir. The create form was not the hole.
I looked at chooser.cgi next because the File Manager listing fix smelled like a sibling. That is a different leftover: a chroot string that starts with the restricted root, then .. inside the listing. Names, sizes, mtimes. Not a user. Not this post.
LDAP Users save_user.cgi backticks mv. That is a command injection leftover, not a prefix. I left it.
The leftover
useradmin/batch_exec.cgi has its own check_user. Create calls it at line 204. Modify calls it at 499. Then create_home_directory mkdir's, or mv moves the old home.
$al = length($access{'home'});
if (length($_[0]->{'home'}) < $al ||
substr($_[0]->{'home'}, 0, $al) ne $access{'home'}) {
return &text('usave_ehomepath', $_[0]->{'home'});
}No slash. No canonicalize. ACL home=/home/a is six characters. /home/ab matches. /home/a/../ab matches too, because it still starts with those six.
save_user.cgi:237 is the sibling that checks:
if (!&is_under_directory($access{'home'}, $user{'home'})) {
&error(&text('usave_ehomepath', $user{'home'}));
}is_under_directory does $dir =~ s/\/*$/\// before the prefix compare. That is the slash check_user never added.
The ACL editor is a text box plus a file chooser. Operators type /home/a. They do not type /home/a/. Defaultacl is home=/, which is a different grant. autohome=1 skips the check on both CGIs. The leftover is the non-slash directory root on the batch path only.
LDAP Users batch copies the same substr. ldap-useradmin/batch_exec.cgi:566. I did not lab that copy.
How the lab was built
Shared image webmin-lab:2.670 from the pin. Entrypoint adds Webmin user limited / lablimited, module useradmin, ACL home=/home/a, ucreate=1, batch=1, makehome=1, autohome off. /home/a exists. /home/ab does not.
Login. GET /useradmin/batch_form.cgi. Multipart POST /useradmin/batch_exec.cgi with source=2 and a create line whose home is /home/ab. Body says Created user h31batch. The directory is there. passwd has the name.
Then POST /useradmin/save_user.cgi with the same home for h31save. The form says you are not allowed to create that home directory. passwd has no h31save. /home/ab is still the batch home.
IOC limited-acl home=/home/a ucreate=1 batch=1 autohome=0
IOC batch_exec status=200 created=1 denied=0
IOC batch-home /home/ab exists=1
IOC save_user status=200 denied=1 passwd=0
SUCCESS WEBMIN-H31-BATCH-PREFIX batch-prefix-home save_user-denied WEBMIN-H31-BATCH-PREFIXLoopback 127.0.0.1:18460. Compose webmin-batch-home-prefix. run.sh clones the tag.
Wrong turns, in the order they happened
Default home=/. That is not a leftover. The KEEP bar is "denied by ACL and still reaches the sink." / denies nothing.
File Manager listing. Closed in 2.670. I wasted a pass confirming the search/chooser prefix was gone, then had to remember batch is a different CGI.
chooser.cgi chroot. Prefix plus .. lists names outside the restricted root. Listing, not user create. Different leftover.
I posted /home/ab to save_user.cgi expecting the same match, because I wanted one oracle. The form denied it. That is the proof the two CGIs disagree. I almost marked the lab FAIL.
autohome=1 skips both checks. I had to strip that key in the entrypoint or the negative would have been noise.
A reverse shell. Theatre.
What an attacker can do
Needs an existing Webmin login that already has Users and Groups, batch create or modify, and a home ACL that does not end in /. They cannot do this with no session. They cannot do this with only the Apache module.
In ordinary words: a coworker who is allowed to make accounts under one directory can make an account whose home sits next to that directory, because the batch importer never put the slash on. The Create User button still stops them.
| Privilege | Method / sink | Recon / integrity / availability | Lab-proven or inferred |
|---|---|---|---|
| Create an OS user whose home is a prefix neighbor of the ACL | Authenticated POST /useradmin/batch_exec.cgi source=2 batch line create:h31batch:...:/home/ab:... |
Integrity (passwd + home dir) | Lab-proven. Body Created user h31batch. /home/ab exists. /etc/passwd has h31batch: |
| mkdir that home as the miniserv user | check_user then create_home_directory |
Integrity (directory create) | Lab-proven. Home did not exist before the POST |
| Same prefix on modify + movehome | check_user then mv of the old home |
Integrity (rename home) | Inferred from source. Lab oracle is create |
/home/a/../ab as a batch home |
same substr of length(access home) |
Integrity | Inferred from source. is_under_directory rejects ..; check_user does not |
| Same helper in LDAP Users batch | ldap-useradmin/batch_exec.cgi check_user |
Integrity | Inferred. Same raw prefix. Not this lab |
| Negative control: Create User form | POST /useradmin/save_user.cgi with home /home/ab |
n/a | Lab-proven. Denied. No h31save in passwd. /home/ab still the batch home |
Stays closed on this leftover
| Axis | Method / sink | Why it is closed |
|---|---|---|
| Integrity | Single-user create / save | save_user.cgi calls is_under_directory |
| Integrity | Trailing-slash ACL home=/home/a/ |
Raw prefix then fails for /home/ab |
| Integrity | autohome=1 |
Both CGIs skip the home-root check. Documented grant |
| Recon | Anonymous batch | Needs a sid and the useradmin module |
| Integrity | File Manager search / chooser listing | Fixed in 2.670 |
| Integrity | chooser.cgi chroot prefix |
Different leftover |
| Integrity | Custom-command injection | Fixed in 2.670 |
The realistic ACL is /home/alice, not /home/a. Batch then matches /home/alice2 and /home/alice-old. The lab used /home/a because six characters make the compare obvious. Same class.
This is not host RCE. This is not a root password. It is an OS user and a directory the ACL said no to, created as the miniserv user, which is usually root.
The fix
Call is_under_directory($access{'home'}, $user->{'home'}) from check_user the way save_user.cgi already does. Do not compare a raw substr of length($access{'home'}). Append the slash, or refuse .., or both. Same change in ldap-useradmin/batch_exec.cgi.
Until then, put a trailing slash on every Users and Groups home ACL that is meant to be a directory root.
References
- github.com/webmin/webmin tag 2.670 (
3a2b2b8671c0669c129f5275b3dd1853106ae02b) useradmin/batch_exec.cgicheck_useruseradmin/save_user.cgiis_under_directoryweb-lib-funcs.plis_under_directory- CWE-22
I am @abraxas_null. Site abraxaslabs.tech. GitHub abraxas. Mail abraxas.null@proton.me.