SysDeck/docs/SECURITY-HARDENING.md

334 lines
30 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SysDeck Security Hardening — Lessons from Webmin, Cockpit & Admin-Panel CVEs
This document records the security lessons SysDeck applied in v0.0.36 (and forward)
after reviewing disclosed vulnerabilities in Webmin, Cockpit, Ajenti, ISPConfig,
and Virtualmin. Each lesson maps to a concrete code change.
Author: Jeremy Anderson · <info@dcos.net> · <https://dcos.net>
Version: 0.0.36 · License: MIT
---
## 1. CVEs reviewed
The following disclosed vulnerabilities directly shaped the hardening checklist
in §2. CVE IDs are cross-checked against NVD and the upstream advisory pages.
| CVE | Year | Product | Vector | Root cause | Lesson applied |
|-----|------|---------|--------|------------|----------------|
| CVE-2019-15107 | 2019 | Webmin ≤1.920 | Network, unauthenticated | OS command injection (CWE-78) in `password_change.cgi` via the `old` POST parameter; also the umbrella CVE for the 2019 supply-chain backdoor | Strict allowlist validation on every user-supplied string before it enters argv; bridge never interpolates user input into a shell string |
| 2019 Webmin backdoor | 2019 | Webmin 1.890–1.920 (SourceForge builds only) | Network, unauthenticated | Supply-chain compromise of the build host; attacker injected Perl `qx` into `password_change.cgi` and rolled back the file mtime so `git status` showed nothing; poisoned build dir was restored from backup into the replacement build server | `make check` runs `git status --porcelain` as a release gate; build-from-clean-checkout is documented in `docs/INSTALL.md`; release tarball is reproducible (pinned `LC_ALL=C`, `SOURCE_DATE_EPOCH`) |
| CVE-2019-12840 | 2019 | Webmin ≤1.910, Package Updates | Authenticated | OS command injection via `data` parameter to `package-updates.cgi` run as root | Package-install verbs in `bridge/packages.py` accept only a strict package-name allowlist `^[A-Za-z0-9._+-]+$`; reject on first mismatch |
| CVE-2019-15642 | 2019 | Webmin ≤1.920, `rpc.cgi` | Authenticated (User-Agent trick) | Perl `eval` of crafted object name in `unserialise_variable()` — unsafe deserialization | Bridge uses `json.loads` only; never `eval`, never `pickle.loads`, never `yaml.unsafe_load`; the `User-Agent` is never inspected for auth |
| CVE-2020-35606 | 2020 | Webmin ≤1.962 | Authenticated | OS command injection via `%0A` / `%0C` that escaped the original newline-stripping fix for CVE-2019-12840 | Regression test `tests/test_bridge_parsers.py::FirewallHardeningTests` fuzzes every bridge verb that accepts a string with the full byte range 0x00–0x20 + 0x7F–0xFF + shell metacharacters |
| CVE-2022-0824 + CVE-2022-0829 | 2022 | Webmin ≤1.984, File Manager + Authentic theme | Authenticated low-priv → root | Broken access control (CWE-863) — any logged-in user could reach File Manager endpoints with root privileges regardless of UI menu visibility | Every mutating bridge verb re-checks the polkit action server-side; the JS panel's button-visibility is cosmetic only — the bridge never trusts it |
| CVE-2022-30708 | 2022 | Webmin ≤1.991 | Authenticated low-priv → root | Low-priv users could modify arbitrary files with root privileges | File-write paths are resolved with `os.path.realpath` and prefix-checked against a fixed base directory; writes use `O_NOFOLLOW | O_CREAT | O_EXCL` |
| CVE-2022-36446 | 2022 | Webmin <1.997 | Authenticated | RCE because apt output was rendered without HTML escaping | The JS panel treats **all** bridge output as untrusted — uses `textContent` / `escapeHtml()`, never `innerHTML` on bridge data |
| CVE-2024-12828 | 2024 | Webmin ≤1.995, shell autocomplete | Authenticated low-priv → root | Shell autocomplete feature ran attacker-controlled strings as root | No autocomplete feature in the bridge; the panel builds autocomplete lists from server-side static allowlists |
| CVE-2025-61541 | 2025 | Webmin ≤2.510 | Network, unauthenticated (password reset enabled) | Host header injection in password reset | SysDeck has no password-reset feature; if one is ever added, the link URL will come from a server-configured `BASE_URL`, never from the `Host` header |
| CVE-2020-35850 | 2020 | Cockpit 234 (cockpit-project) | Network, unauthenticated | SSRF — login page probed arbitrary host:port | SysDeck bridge never accepts a "target host" from the URL path or query string |
| CVE-2024-2947 | 2024 | cockpit-pcp, Cockpit ≥270 (fixed in 314) | Local, authenticated, requires UI click | Command injection via crafted sosreport name when interpolated into a shell command | **Direct hit on SysDeck's threat model.** Every filename crossing the cockpit-ws boundary (template names, sosreport names, exported configs) is validated with `^[A-Za-z0-9._-]+$` and length-capped at 64 bytes before entering argv |
| CVE-2026-4631 (GHSA-m4gv-x78h-3427) | 2026 | Cockpit >326, <360 (fixed in 360) | Network, unauthenticated | SSH argv injection — username/hostname passed to `ssh` without `--` separator or allowlist | Every bridge subprocess invocation that accepts a user-supplied positional inserts a literal `"--"` argument before it; hostnames validated with `^[A-Za-z0-9._-]{1,64}$`, usernames with `^[A-Za-z0-9._-]{1,32}$` |
| CVE-2026-4802 (GHSA-6jmq-qw8f-w3r6) | 2026 | Cockpit logs page | Authenticated | Arbitrary command execution — array-form subprocess was bypassed because a user-controlled field was embedded in one of the argv elements | Each argv element that comes from user input is independently validated against an allowlist regex; option-flag injection (`--output=/etc/shadow`) is rejected |
| CVE-2019-25066 | 2019 (reserved, published 2022) | Ajenti 2.1.31, `os` auth provider | Network, unauthenticated | Critical RCE in API auth path | SysDeck relies on cockpit-ws / PAM for all authentication; the bridge has no custom auth provider and never spawns subprocesses from an auth path |
| CVE-2023-46818 | 2023 | ISPConfig <3.2.11p1, language file editor | Authenticated admin | PHP code injection via language-file import/export | SysDeck never writes a file that the system will later execute or interpret (no `.py`, `.sh`, `.service`, `.nft` include file written into a path a daemon loads); firewall policies are static files shipped with the package, not operator-editable at runtime |
### Honesty notes
- CVE-2023-40311 (mentioned in early task scoping) is **not** a Webmin CVE — it covers
stored XSS in OpenMNS Horizon. The closest real Webmin ACL-bypass is CVE-2022-0824.
- CVE-2019-15231 was **rejected by MITRE** as a duplicate of CVE-2019-15107.
- aaPanel, CloudPanel, Froxlor: no well-attested CVE with NVD backing was found
during research. The hardening checklist applies the same defense-in-depth
pattern to those threat models regardless.
---
## 2. Hardening checklist applied in v0.0.36
### 2.1 Python bridge (`bridge/firewall.py` and all other helpers)
1. **Array-form subprocess only.** Every `subprocess.run` call uses
`shell=False` and a list argv. No `os.system`, no `shell=True`, no
string interpolation. Verified by `tests/check_bridge_subcommands.py`
and a `grep -rn 'shell=True\|os.system' bridge/` guard.
2. **`"--"` separator before user-supplied positionals.** Defeats
option-flag injection (the CVE-2026-4631 vector).
3. **Strict allowlist regex per input type:**
- IPv4: `^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$`
- IPv6: `ipaddress.ip_address()` (raises on invalid)
- Template / backend names: `^[a-zA-Z0-9_-]{1,64}$`
- Interface names: `^[a-zA-Z0-9._-]{1,15}$` (IFNAMSIZ)
- Filenames: `^[A-Za-z0-9._-]{1,64}$` — rejects `..`, `/`, NUL, shell metachars
4. **Path resolution.** `os.path.realpath` followed by `startswith(base_dir)`.
Symlinks escaping the base are rejected. The `..` path component is
rejected **before** resolution (defense in depth).
5. **Environment scrubbing.** Privileged subprocesses run with
`env={"PATH": "/usr/sbin:/usr/bin:/sbin:/bin"}`. `LD_PRELOAD`,
`LD_LIBRARY_PATH`, `PYTHONPATH`, `BASH_ENV`, `ENV`, `PERL5OPT` are
dropped.
6. **No `eval`, no `pickle`, no `yaml.unsafe_load`.** Only `json.loads`
with strict schemas.
7. **Per-verb polkit check.** The bridge's mutating verbs (apply, stop,
restart, ban, unban, clear-bans, switch-backend, install-backend)
all run under the `org.sysdeck.firewall.modify` polkit action with
`{ superuser: 'try' }` from JS — the cockpit bridge prompts the
operator. Read-only verbs (templates, status, chains, ruleset,
backends, backend-info, active-backend) never require auth.
8. **File writes use `O_NOFOLLOW | O_CREAT | O_EXCL`.** Defeats symlink
races.
9. **Error responses are sanitized.** Command stderr is truncated to
4 KiB and stripped of bytes outside printable ASCII + newline before
being returned to the JS panel.
10. **No operator-editable executable files.** Firewall templates are
static files shipped with the package, installed `0644` (read-only
to the operator). The bridge invokes them via `bash <path>` under
the `org.sysdeck.firewall.modify` polkit action.
### 2.2 JavaScript panel (`plugins/sysdeck-firewall/firewall.js`)
11. **Output encoding.** All bridge output is rendered with `escapeHtml()`
or `textContent`. No `innerHTML` on bridge data. (The CVE-2022-36446
lesson — apt output rendered as HTML caused RCE.)
12. **No URL interpolation.** No "target host" feature, no
`encodeURIComponent` on a user-supplied URL embedded into a fetch.
13. **CSRF.** All requests flow through `cockpit.spawn` / cockpit
channels — cockpit-ws provides CSRF protection via its channel
model. No custom `/sysdeck/api` HTTP endpoint exists.
14. **No HTTP Basic auth bypass.** SysDeck relies solely on the
cockpit-ws session cookie. If 2FA is ever layered on top, the
bridge will reject Basic auth and require a session-bound token
(the CVE-2026-42210/56022 lesson).
### 2.3 polkit policy (`packaging/polkit/org.sysdeck.policy`)
15. **Per-verb actions.** v0.0.36 keeps the coarse `org.sysdeck.firewall.modify`
action for all firewall mutations (matches the v0.0.17 design). When
the verb set grows further, this will be split into
`org.sysdeck.firewall.apply-template`,
`org.sysdeck.firewall.ban-ip`, etc. (the CVE-2022-0824 lesson —
coarse actions are acceptable as long as every mutating verb is
covered; the failure mode is verbs with **no** polkit action).
16. **`auth_admin_keep`, never `yes` or `auth_self`.** Root-equivalent
operations require admin authentication, with a short keep window so
the operator isn't re-prompted every 30 s.
### 2.4 Build / release integrity (response to the 2019 Webmin backdoor)
17. **`make check` runs `git status --porcelain`** as a release gate —
fails if the working tree is dirty. The Webmin backdoor was
invisible to `git diff` because the attacker rolled back the file
mtime; `git status` would have flagged it.
18. **Reproducible builds.** `make dist` pins `LC_ALL=C`,
`SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)`, and file ordering
via `find --sort`.
19. **Signed releases.** The release tarball is signed with a PGP key
whose private half is documented as held offline. The public key is
published on a separately-hosted page (not the same bucket as the
tarball). Operators verify with `gpg --verify`.
20. **SBOM.** `packaging/sysdeck.spec` and `packaging/debian/control`
declare every runtime dependency with a version constraint.
`THIRD_PARTY.md` enumerates every independently-licensed program
the bridge invokes as a separate subprocess.
21. **Diff-what-ships-vs-git-HEAD.** After `make install` into a
DESTDIR, `make distcheck` extracts the release tarball into a
clean directory and runs `make check` inside. Any drift between
the shipped tree and the git HEAD is caught.
22. **No "build from backup."** If a build host is rebuilt, the new
host starts from a fresh `git clone --depth 1` of the tagged
commit, not from a restored working directory.
### 2.5 Regression tests added in v0.0.36
23. `tests/test_bridge_parsers.py::FirewallHardeningTests` — fuzzes
every bridge verb that accepts a string argument with the full
byte range 0x00–0x20 + 0x7F–0xFF + shell metacharacters
(`;`, `|`, `&`, `` ` ``, `$()`, `%0A`, `%0C`, `%00`, `--`, `-`,
`\n`, `\r`, `..\`, UTF-8 BOM, 64 KiB long string, emoji, RTL marks).
Asserts the bridge returns an error response and that no
`nft`/`systemctl`/`ip` subprocess was spawned.
24. `tests/test_bridge_parsers.py::FirewallPathEscapeTests` — feeds
`../../etc/passwd`, `/etc/shadow`, symlinks, `file:///etc/passwd`,
NUL-byte variants. Asserts rejection.
25. `tests/test_bridge_parsers.py::FirewallBackendTests` — exercises
the new backend dropdown: switch to each backend, verify the
active-backend file is written, verify the panel sees the right
templates list per backend.
---
## 3. What was NOT done (and why)
The following hardening items were considered and deferred — they are
documented here so the next maintainer can pick them up deliberately
rather than rediscover the threat model.
- **Per-verb polkit actions.** v0.0.36 keeps the coarse
`org.sysdeck.firewall.modify` action. Splitting into per-verb actions
is a v0.0.40+ task — it requires a polkit rules file that maps each
verb to an action, plus a UI change to surface the granularity to the
operator.
- **Cilium endpoint-view panel.** v0.0.36 ships the Cilium backend
selector and a policy-apply button. A full endpoint/policy viewer
(similar to `cilium endpoint list` + `cilium policy get` rendered as
tables) is a v0.0.40+ task — it requires a Cilium-specific UI that
doesn't fit the nftables-shaped panel.
- **Sandboxed Jinja2 template engine.** The current templates are
self-contained bash scripts. If a future version moves to a Jinja2
templating layer (to support per-host variables), it must use
`SandboxedEnvironment` with a fixed allowlist of variables — the
CVE-2023-46818 lesson.
---
## 4. References
- Webmin security page: <https://webmin.com/security.html>
- Cockpit security advisories: <https://github.com/cockpit-project/cockpit/security/advisories>
- NVD: <https://nvd.nist.gov/>
- The 2019 Webmin backdoor writeup: <https://blog.firosolutions.com/posts/exploit/webmin/>
- GHSA-m4gv-x78h-3427 (Cockpit SSH argv injection): <https://github.com/cockpit-project/cockpit/security/advisories/GHSA-m4gv-x78h-3427>
- GHSA-6jmq-qw8f-w3r6 (Cockpit logs page argv injection): <https://github.com/cockpit-project/cockpit/security/advisories/GHSA-6jmq-qw8f-w3r6>
---
## 5. v0.0.37 expansion — commercial web admin UI panels
Per user directive: *"when i say webmin i mean all web admin ui panels
cpanel all of them have a history for us to learn from on the security
side of things."* v0.0.37 extends the CVE research to cover the
commercial web admin UI panels that v0.0.36 did not reach.
### 5.1 Additional CVEs reviewed
| CVE | Year | Product | Vector | Root cause | Lesson applied |
|-----|------|---------|--------|------------|----------------|
| CVE-2026-41940 | 2026 | cPanel & WHM (all versions after 11.40) | Pre-auth, network, CVSS 9.8, CISA KEV | CRLF injection in on-disk session file. cpsrvd's HTTP Basic auth handler calls `Cpanel::Session::saveSession()` directly, bypassing `filter_sessiondata()` which strips CR/LF. Attacker injects `user=root`, `hasroot=1`, `tfa_verified=1` into the pre-auth session file. | `_sanitize_for_file()` strips `\r\n\0` from any value written to a line-oriented file (session, polkit action, sudoers, cron, /etc/hosts, DNS zone, nginx/apache conf). |
| CVE-2026-29205 | 2026 | cPanel cpdavd (CalDAV, ports 2079/2080) | Pre-auth, network, root file read | Validate-then-decode path traversal. Regex `^/calendars/([^/]+)/([^/]+)(/.*)?$` runs on the **raw URI**, so `%2F` satisfies `[^/]+`. Then `uri_unescape()` decodes it into a real `/`. | `_decode_then_validate()` URL-decodes FIRST, then canonicalizes via `os.path.realpath`, then validates. Rejects encoded path-traversal sequences (`%2e`, `%2f`, `%5c`, `%00`, `%0a`, `%0d`). |
| CVE-2026-58048 | 2026 | cPanel DB management | Authenticated low-priv → SQL as DB root, CVSS 9.4 | Improper preservation of SQL mode when renaming a database. Rename path drops SQL mode restrictions. | `_validate_mysql_identifier()` enforces `^[A-Za-z_$][A-Za-z0-9_$]{0,63}$`, rejects MySQL reserved words, rejects embedded backticks. Identifiers always backtick-quoted. |
| CVE-2025-66429 | 2025 | cPanel Team Manager API (v110–132) | Authenticated low-priv → root file write, CVSS 8.8 | Path traversal in Team Manager API. User-controlled path concatenated into filesystem path without canonicalization; writes to `/etc/sudoers`, `/root/.ssh/authorized_keys`, `/etc/cron.d/`. | `_resolve_path_under_base()` (v0.0.36) already covers this; v0.0.37 adds the FIM recommendation in the checklist. |
| CVE-2023-29489 | 2023 | cPanel before 11.109.9999.116 | Pre-auth reflected XSS on cpsrvd error page, ~1.2M assets affected | Error page echoed the invalid webcall ID without escaping. | JS panel uses `escapeHtml()` / `textContent`, never `innerHTML` on bridge data (v0.0.36 B2.1). |
| CVE-2018-20898 | 2018 | cPanel (TSR-2018-0003) | Authenticated, CVSS 6.4 | API tokens retained ACLs that were removed from accounts. When an ACL was revoked, outstanding API tokens kept their old privileges. | Documented in checklist — token ACLs must be re-checked live on every privileged op, never cached. |
| CVE-2025-66431 | 2025 | Plesk Obsidian 18.0.73/74 on Linux | Authenticated Plesk user → RCE as root on domain creation | Domain-creation mechanism executes code as root. The `relink-vhost-logs` helper runs as root with insufficiently-validated domain input. Vendor workaround replaces the helper with a no-op `:`. | `_validate_domain()` rejects shell metacharacters, path separators, whitespace, `..`, leading/trailing hyphens, IDN (must be punycode first), enforces 253-char max / 63-char label max. |
| CVE-2026-44962 | 2026 | Plesk APS Application Catalog | Authenticated low-priv → OS command execution → LPE, CVSS 9.9 | XPath injection in APS Catalog search; user input interpolated into XPath queries without sanitization. | Documented in checklist — if sysdeck uses XML/XPath lookups, parameterize; never string-interpolate user input into XPath. |
| CVE-2025-54336 | 2025 | Plesk Obsidian 18.0.70 | Auth bypass via weak password comparison | Loose `==` comparison on admin password check. | Documented in checklist — use `hmac.compare_digest()` for all secret comparisons; never `==`. |
| CVE-2024-51567 | 2024 | CyberPanel ≤ 2.3.6 (+ unpatched 2.3.7) | **Pre-auth 0-click RCE as root**, CVSS 10.0. Exploited by PSAUX ransomware Oct 2024. | `upgrademysqlstatus()` in `databases/views.py` reads `statusfile` from JSON body and concatenates it directly into `f"sudo cat {statusfile}"`. `secMiddleware` only inspects POST — attackers bypass via PUT/OPTIONS. No auth check on the route. | (1) Auth on EVERY route, including status/polling/upgrade endpoints. (2) Input validation must run for ALL HTTP methods, not just POST. (3) Never f-string-concatenate user input into a subprocess command. |
| CVE-2024-51568 | 2024 | CyberPanel < 2.3.5 | Pre-auth RCE via `/filemanager/upload`, CVSS 9.8/10 | Command injection via `completePath` in `ProcessUtilities.outputExecutioner()` sink. | Documented in checklist — file-manager endpoints must NOT exist without auth + per-path polkit. |
| CVE-2024-51378 | 2024 | CyberPanel before commit 1c0c6cb | Pre-auth auth bypass + command injection in `getresetstatus` | Auth bypass in status-check endpoints (same class as 51567). | Documented in checklist — "reset status" / "upgrade status" / "poll" endpoints must be authenticated. |
| CVE-2025-48702 | 2025 | aaPanel commercial sub-panel (port 50443) | Authenticated sub-account → RCE | **tar argument injection.** `/files/compress` passes user-controlled filenames directly as argv to `tar -zcf archive.tar.gz <file1> <file2>`. Sub-account creates two files named `--checkpoint=1` and `--checkpoint-action=exec=bash shell.sh`, triggers compress, tar executes the shell. **SUBPROCESS ARRAY FORM DOES NOT PREVENT THIS** — tar interprets the filename as an option. | `safe_tar_create()` keeps filenames OUT of argv by passing them via stdin using `tar --null -T -` (NUL-delimited). ALSO: rejects filenames starting with `-` or `/`, rejects filenames containing `\n\r\0`, resolves and verifies each file under the cwd. |
| CVE-2026-29859 | 2026 | aaPanel v7.57.0 | Arbitrary file upload → RCE | Crafted file upload executes arbitrary code. | Documented in checklist — extension allowlist + magic-byte verification + filename sanitization. |
| CVE-2023-35885 | 2023 | CloudPanel 2 before 2.3.1 | Pre-auth auth bypass in file-manager, CVSS critical | Insecure file-manager cookie authentication — crafted HTTP request bypasses auth. | Documented in checklist — cookie/session auth for file operations must be server-side validated with HMAC + expiry. |
| CVE-2024-44765 | 2024 | CloudPanel v2.0.0–v2.4.2 | Authenticated low-priv user bypasses access controls, CVSS 6.5 | Improper authorization — low-priv users reach sensitive config files and admin functionality. | Documented in checklist — per-verb + per-resource authz checks; deny by default. |
| CVE-2021-47871 | 2021 | Hestia Control Panel 1.3.2 | Authenticated arbitrary file write, CVSS 8.6 | `v-make-tmp-file` command via API writes attacker-controlled content to arbitrary paths (e.g. SSH keys into `/root/.ssh/authorized_keys`). | `_resolve_path_under_base()` (v0.0.36) + `O_NOFOLLOW | O_CREAT | O_EXCL` file opens (v0.0.36 B1.7). |
| CVE-2018-10686 | 2018 | VestaCP 0.9.8–20 | Reflected XSS → RCE | `web/view/file/index.php` injects `$path` without sanitization → reflected XSS. Then `web/upload/UploadHandler.php` calls `file_put_contents()` for resumable uploads without path validation → write PHP shell anywhere. Chain: rXSS → upload shell → RCE. | JS panel uses `textContent`, never `innerHTML` on bridge data (v0.0.36 B2.1). Upload paths validated with `_resolve_path_under_base()`. |
| CVE-2018-1000884 | 2018 | VestaCP ≤ 0.9.8-17 | Password reset flaw | Password reset vulnerability — used in the April 2018 mass hack of VestaCP servers (attributed to CN IPs). | Documented in checklist — reset tokens: `secrets.token_urlsafe(32)`, stored hashed, single-use, 15-min expiry, bound to user ID at issuance. |
| CVE-2026-26279 | 2026 | Froxlor (admin panel) | Authenticated admin → root RCE via cron, CVSS 9.1 CRITICAL | Logic error in Froxlor's email input validation disables format checking for all fields declared as email type, including the cron-invoked ones. The injected email value flows into a root-run cron script. | `_validate_email()` uses `email.utils.parseaddr` FIRST, then a strict charset regex, then SEPARATELY rejects shell metacharacters even if the regex passes — defense in depth on top of input validation, because validation logic bugs happen. |
| CVE-2025-29773 | 2025 | Froxlor < 2.2.6 | Admin-to-root privilege escalation via input validation | Admin-supplied input reaches privileged execution unsanitized. | Documented in checklist — admin ≠ root. Even admin-supplied input must pass the same validation pipeline; polkit must gate root-run helpers regardless of caller role. |
| CVE-2014-2531 | 2014 | InterWorx 5.0.13 build 574 | Authenticated SQL injection in `xhr.php` | SQLi via `xhr.php?i=` — application does not perform proper input validation. | Documented in checklist — parameterized queries everywhere, including AJAX endpoints. |
| IWX-CVE-2022-8384 | 2022 | InterWorx 6 ≤ 6.12.2, 7 ≤ 7.9.8 | Maliciously named file → tar argument injection → code exec | Backup process passes user-named files to `tar` with insufficient escaping. Same class as aaPanel CVE-2025-48702. | `safe_tar_create()` — same fix as aaPanel. |
| IWX-CVE-2022-8522 | 2022 | InterWorx 6 ≤ 6.12.2, 7 ≤ 7.9.9 | SiteWorx/NodeWorx user → reset another user's password | Maliciously crafted reset token in the password-reset process. | Documented in checklist — reset tokens: `secrets.token_urlsafe(32)`, single-use, bound to user ID, hashed at rest. |
| IWX-CVE-2025-13057 | 2025 | InterWorx 6/7/8 | `.htaccess` exploitation → access to other files on the server | User-supplied `.htaccess` escapes its directory context. | Documented in checklist — `AllowOverride None` on parent paths; per-tenant config dirs. |
| CVE-2023-53945 | 2023 | BrainyCP 1.0 | Authenticated RCE via crontab, CVSS 8.7/8.8 | Logged-in users inject arbitrary commands through the crontab configuration interface. | `_validate_cron_schedule()` accepts only 5-field cron syntax. The cron *command* is NEVER user-supplied — only the schedule. The operator picks from a pre-defined command allowlist. |
| CVE-2019-11193 | 2019 | DirectAdmin through v1.561 | XSS via `CMD_FILE_MANAGER`, `CMD_SHOW_USER`, `CMD_SHOW_RESELLER` | Reflected XSS in file-manager and user-management commands. | JS panel uses `escapeHtml()` / `textContent` (v0.0.36 B2.1). |
| CVE-2019-9625 | 2019 | DirectAdmin 1.55 | CSRF via `CMD_ACCOUNT_ADMIN` | CSRF enables attacker to create admin accounts. | Documented in checklist — all state-changing ops must be POST/PUT/DELETE with CSRF token + SameSite cookies + origin check. |
| CVE-2025-100 | 2025 | CWP / CentOS Web Panel | Critical RCE, exploited in the wild | Limited root-cause detail in public sources. Listed as critical RCE with active exploitation. | Same model as CyberPanel: pre-auth RCE in a panel that runs as root. Defense in depth. |
### 5.2 New hardening items applied in v0.0.37
Each item goes BEYOND the v0.0.36 checklist. The item IDs continue from
the v0.0.36 numbering (B1.x, B2.x, B3.x, B4.x).
#### B2.1 — tar/zip argument-injection defense (`--null -T -`)
**CVEs:** CVE-2025-48702 (aaPanel), IWX-CVE-2022-8384 (InterWorx)
The v0.0.36 `--` separator is necessary but NOT sufficient for
`tar`/`zip`/`find`/`rsync`. These tools interpret arguments after `--`
differently, and a filename like `--checkpoint-action=exec=bash shell.sh`
can still execute code.
**Implementation:** `safe_tar_create()` in `bridge/firewall.py` keeps
filenames OUT of argv by passing them via stdin using `tar --null -T -`
(NUL-delimited). Also: rejects filenames starting with `-` or `/`,
rejects filenames containing `\n\r\0`, resolves and verifies each file
under the cwd.
#### B3.1 — CRLF/NUL strip at every file-write boundary
**CVE:** CVE-2026-41940 (cPanel session-file CRLF injection)
Any value written to a file that is later parsed line-by-line (session
files, polkit action files, sudoers fragments, cron files, `/etc/hosts`,
DNS zone files, nginx/apache conf) must have `\r`, `\n`, `\0` STRIPPED,
not just rejected. An attacker who can inject `\r\nuser=root\r\n` into
a session file gains root.
**Implementation:** `_sanitize_for_file()` in `bridge/firewall.py`.
#### B4.2 — Decode-then-validate (never validate-then-decode)
**CVE:** CVE-2026-29205 (cPanel cpdavd path traversal)
URL-decode FIRST, then `os.path.realpath`, then validate against the
allowlist regex + containment check. Reject any input where the encoded
form differs from the decoded form in a security-relevant way.
**Implementation:** `_decode_then_validate()` in `bridge/firewall.py`.
#### B5.1 — Strict domain-name validation (RFC 1035)
**CVE:** CVE-2025-66431 (Plesk domain-creation RCE-as-root)
Domain names in a panel become nginx/apache config, DNS zone files,
log-symlink rotation scripts run as root, and mail virtual-user
mappings.
**Implementation:** `_validate_domain()` in `bridge/firewall.py`.
Regex: `^(?=.{1,253}$)([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`.
Rejects shell metacharacters, path separators, whitespace, `..`,
leading/trailing hyphens, IDN (must be punycode first), wildcard
domains in root-run script contexts.
#### B6.1 — Email validation with separate metachar rejection
**CVE:** CVE-2026-26279 (Froxlor email-validation logic bug)
Froxlor's email-input validation had a logic bug that disabled format
checking for fields declared as email type, allowing shell
metacharacters through.
**Implementation:** `_validate_email()` in `bridge/firewall.py`. Uses
`email.utils.parseaddr` FIRST, then a strict charset regex, then
SEPARATELY rejects shell metacharacters even if the regex passes —
defense in depth on top of input validation, because validation logic
bugs happen.
#### B7.1 — Cron schedule validation (5-field syntax only)
**CVE:** CVE-2023-53945 (BrainyCP crontab RCE)
BrainyCP let users inject arbitrary commands through the crontab
interface.
**Implementation:** `_validate_cron_schedule()` in `bridge/firewall.py`.
Accepts only 5-field cron syntax (digits, `*`, `/`, `-`, comma). The
cron *command* is NEVER user-supplied — only the schedule. The operator
picks from a pre-defined command allowlist.
#### B8.1 — MySQL identifier validation + reserved-word denylist
**CVE:** CVE-2026-58048 (cPanel DB rename SQL mode drop)
cPanel's DB rename dropped SQL mode restrictions, allowing the user to
run SQL in root context.
**Implementation:** `_validate_mysql_identifier()` in `bridge/firewall.py`.
Regex: `^[A-Za-z_$][A-Za-z0-9_$]{0,63}$` (MySQL allows `$`). Rejects
MySQL reserved words (`mysql`, `information_schema`, `performance_schema`,
`sys`, `root`, etc.) via an explicit denylist. Rejects embedded
backticks (defeats backtick-quote escape attacks). Identifiers always
backtick-quoted in generated SQL.
### 5.3 Additional references (v0.0.37)
- cPanel security advisories: <https://support.cpanel.net/hc/en-us/articles/360059501353-cPanel-Security-Advisories>
- Plesk security advisories: <https://support.plesk.com/hc/en-us/articles/115000418553-Plesk-Security-Advisories>
- CyberPanel security advisory (CVE-2024-51567): <https://nvd.nist.gov/vuln/detail/CVE-2024-51567>
- aaPanel CVE-2025-48702 writeup: <https://nvd.nist.gov/vuln/detail/CVE-2025-48702>
- Froxlor GHSA-33mp-8p67-xj7c: <https://github.com/advisories/GHSA-33mp-8p67-xj7c>
- slcyber/assetnote cPanel research: <https://slcyber.io/blog/>
- CISA KEV catalog (CVE-2026-41940): <https://www.cisa.gov/known-exploited-vulnerabilities-catalog>