SysDeck/docs/SECURITY-HARDENING.md

30 KiB
Executable File
Raw Blame History

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
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)

  1. 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.)
  2. No URL interpolation. No "target host" feature, no encodeURIComponent on a user-supplied URL embedded into a fetch.
  3. 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.
  4. 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)

  1. 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).
  2. 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)

  1. 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.
  2. Reproducible builds. make dist pins LC_ALL=C, SOURCE_DATE_EPOCH=$(git log -1 --format=%ct), and file ordering via find --sort.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. 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.
  2. tests/test_bridge_parsers.py::FirewallPathEscapeTests — feeds ../../etc/passwd, /etc/shadow, symlinks, file:///etc/passwd, NUL-byte variants. Asserts rejection.
  3. 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


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
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)