# 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 · · 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 ` 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: - Cockpit security advisories: - NVD: - The 2019 Webmin backdoor writeup: - GHSA-m4gv-x78h-3427 (Cockpit SSH argv injection): - GHSA-6jmq-qw8f-w3r6 (Cockpit logs page argv injection): --- ## 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 `. 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: - Plesk security advisories: - CyberPanel security advisory (CVE-2024-51567): - aaPanel CVE-2025-48702 writeup: - Froxlor GHSA-33mp-8p67-xj7c: - slcyber/assetnote cPanel research: - CISA KEV catalog (CVE-2026-41940):