SubMgr ≤ 3.42 — pre-authentication RCE chain + 5 further issues (CVE-2026-94832 … CVE-2026-94838)
Summary
SubMgr is a PHP submission-manager used by literary magazines, journals and university departments. Seven issues; two of them chain into pre-authentication remote code execution on installs running PHP < 8.1.
public submission form, `email` field CVE-2026-94832 blind SQLi → admin bcrypt hash
↓ (PHP < 8.1 only)
Config > General, `mysqldump_path` CVE-2026-94835 shell_exec() command injection
↓
Maintenance > Backup arbitrary code as the web user
Five further issues stand alone: two more SQL injections, an admin path traversal, and two file-upload holes — one of which is reachable unauthenticated.
Affected
| Product | SubMgr (Submission Manager), Devin Emke |
| Affected | all versions through 3.42 |
| Fixed | commit 658d46c8 on main, 2026-04-18 |
| Chain precondition | PHP < 8.1 for link 1 |
The chain
CVE-2026-94832 — pre-auth blind SQL injection via email
CWE-89 · CVSS 3.1 8.1 (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H)
Three facts have to line up, and on PHP 7.x they do:
email_check()atinc_common.php:1544validates with a regex that explicitly permits single quotes in the local part — legal per RFC 5321, fatal here.- The value is passed through
htmlspecialchars(). Before PHP 8.1 the default flag set wasENT_COMPAT, which does not encode'. PHP 8.1 changed the default toENT_QUOTES|ENT_SUBSTITUTE, which does — and that single language change is the only thing standing between a modern install and this bug. - The unescaped value is interpolated into a query at
inc_common.php:1664.
The email regex also permits /, * and #, which is everything an attacker needs: /**/
substitutes for whitespace and # terminates the statement. Boolean-based blind extraction of
the full 60-character bcrypt admin hash takes roughly 2000 HTTP requests against the public
submission form, with no account.
CVE-2026-94835 — OS command injection in the database backup function
CWE-78 · CVSS 3.1 7.2 (AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H)
An admin sets mysqldump_path under Configuration > General. On save it passes through
strip_tags() and stripslashes() — neither of which touches a shell metacharacter. At run
time there are exactly two guards:
strpos($mysqldump_path, 'mysqldump')— the string must contain "mysqldump".- if the value contains a space, it gets double-quoted.
Both are trivially satisfied: keep the literal mysqldump somewhere in the payload, and use
${IFS} instead of spaces so the quoting branch never fires. The value reaches shell_exec()
at inc_login.php:1302 when Maintenance > Backup runs.
strip_tags() survives a base64 round-trip, so the reliable primitive is
echo <b64>|base64${IFS}-d>file;#.
Chained: 94832 yields the admin hash → crack or replay → 94835 is a web shell.
The other five
CVE-2026-94833 — popup.php: extract($_REQUEST) → unconstrained SQL injection
CWE-89 / CWE-621 · CVSS 3.1 7.5 unconditional / 9.8 with MySQL FILE
Line 9 is extract($_REQUEST), which overwrites every local and global variable with
attacker-supplied values. Line 17 then builds:
"SELECT $field FROM `$table` WHERE $id_name = $id_value"
Four of the five components are now attacker-controlled, with no character restrictions and
full query control — and the result is rendered back on the page, so it is a direct read, not
a blind one. If the MySQL user holds FILE, INTO OUTFILE writes a webshell to the webroot.
Authorization is $_SESSION['contact']['access'] being truthy — and the
extract($_REQUEST) at popup.php:9 runs six lines before that check, at global scope, so
the request overwrites the $_SESSION superglobal itself: &_SESSION[contact][access]=zzzz
satisfies the popup.php:15 gate with no cookie and no account, and the forged value need
not be a real role. This is unauthenticated. Controls: parameter absent → no output;
parameter empty → no output; a real logged-in submitter without the forge → no output.
The extract() also overwrites $config, $nonce and friends. Lower impact, but it is the
same defect.
CVE-2026-94836 — SQL injection in the tag bulk-action handler
CWE-89 · CVSS 3.1 6.5
inc_login.php:572 implode()s $_POST['tag'][] directly into an IN() clause with no
escaping:
tag[]=1) UNION SELECT password,2,email,4,5 FROM contacts--
A second unsanitized implode() at inc_login.php:579 consumes values derived from the first
query's results — not independently exploitable, since those come from the database.
CVE-2026-94834 — arbitrary file read in download.php
CWE-22 · CVSS 3.1 4.9
download.php:40 concatenates $upload_path . $_GET['year'] . $_GET['file'] with no traversal
check. Admin-only, and an admin already holds a lot — but it crosses the trust boundary out of
the upload directory: file=../../config_db.php yields the database credentials,
file=../../../etc/passwd the system file.
CVE-2026-94837 — TOCTOU in file upload: written before validated
CWE-434 / CWE-367 · CVSS 3.1 7.5
upload() at inc_common.php:1708 moves the file into the upload directory first and
form_check() validates the extension second. When validation fails, exit_error() fires —
and the file is still on disk. It is only cleaned up if the same session uploads again.
The resulting name is temp_<unix_timestamp>_<attacker_chosen_name>.php, and the timestamp
comes straight out of the HTTP Date response header. Both halves are known.
Reachable from the public, unauthenticated submission form at main_home.php:30, as well as
the authenticated one at main_login.php:396. Exploitation needs upload_path to be
web-accessible; SubMgr's documentation recommends putting it outside the webroot but does not
enforce it.
CVE-2026-94838 — action attachments: no extension validation at all
CWE-434 · CVSS 3.1 8.8
Submission uploads at least check an allowlist (the file_types table). The action attachment
path does not check anything. main_login.php:1319 calls upload() and then tests only for
upload errors and size. The original extension is preserved when the file is permanently renamed
to action_<id>.<ext> at main_login.php:694, and action_id is sequential.
So: any staff account — admin and editor unconditionally, and an active 1–active 5
account whenever its group digit appears in the chosen action type's access_groups
(main_login.php:1334) — attaches shell.php to a submission and fetches
action_<n>.php. Permanent file, guessable name, no validation.
Remediation
Update to main at commit 658d46c8 (2026-04-18) or later.
There is no tagged release containing these fixes. The in-tree version string reads
3.42 both before and after the fix commit. If you installed from a tag, a release archive or
a vendor bundle, you are almost certainly still vulnerable even though you believe you are on
the latest version. Check the commit, not the version string.
Fixes as landed:
| CVE | File:line on main |
Fix |
|---|---|---|
| 94832 | inc_common.php:1600 |
real_escape at the email SQL sink |
| 94833 | popup.php:17-29 |
extract() removed; parameter allowlist; (int); real_escape |
| 94834 | download.php:34-36 |
basename + realpath + (int) |
| 94835 | inc_login.php:1302 |
escapeshellcmd |
| 94836 | inc_login.php:572 |
(int) + real_escape on tag[] |
| 94837 | inc_common.php:1567 |
temp file unlinked on invalid extension |
| 94838 | main_login.php:1349-1354 |
extension allowlist + MIME check + temp delete |
Operator hardening regardless of version:
- Put
upload_pathoutside the webroot. This alone defeats 94837 and 94838. The docs recommend it; nothing enforces it. - Run PHP ≥ 8.1. It closes CVE-2026-94832 as a side effect of the
htmlspecialchars()default change — which is luck, not defence, but take it. - Do not grant the application's MySQL user
FILE.
Residual, not exploitable — noted for completeness
exclude_file_types(inc_common.php:39) is a denylist and missesphtml,php3–php7,phps,pht,phar,shtml. The allowlist still gates, so this is defence-in-depth only.extract()remains inform_check, but on$GLOBALSand not attacker-controlled.- 94837's fix leaves a sub-millisecond move-before-validate race.
Timeline
| Date | Event |
|---|---|
| 2026-03-28 | Seven issues reported to the vendor |
| 2026-04-18 | Vendor ships 658d46c8 fixing all seven, crediting [@davidkm-ai] |
| 2026-06-06 | Fixes independently verified at every sink |
| 2026-10-03 | MITRE assigns CVE-2026-94832 … CVE-2026-94838 |
| 2026-10-21 | This write-up published |
Note for operators: all seven issues were fixed on the main branch, but no tagged
release carries the fixes and the in-tree version string is unchanged at 3.42 both before and
after the fix commit. Installations deployed from a tag or a source archive therefore remain
affected. Deploy from main at or after 658d46c8, or apply the commit directly.
Want this depth on your own systems?
DKM Security does the same work against client infrastructure, under scope and under contract.
Get In Touch