# Server settings validation (2026-09-30) Problem: values of the server/PKI settings (`PUT /profiles-api/config/server|pki`) were free strings that were rendered into `server.conf` (written by a root process) and passed to `easyrsa`. A user with a valid token could inject extra OpenVPN directives (for example `up`/`plugin`) or shell-relevant DN characters, which is a path to code execution as root. ## Defence in three layers | Layer | Where | What it does | |---|---|---| | A. Schema | `APP_PROFILER/schemas.py` (`SystemSettingsUpdate`, `PKISettingUpdate`) | Rejects invalid values with 422. Applies to updates only, so already stored values never break `GET /config` | | B. Scripts | `services/validation.py: check_script`, used in `services/generator.py` | `connect_script`/`disconnect_script` must be a file directly inside `/etc/openvpn/scripts/`, owned by root, not group/other-writable, in a root-owned non-writable directory (symlinks out of the directory are rejected) | | C. Renderer | `services/generator.py` | Before writing `server.conf` / client `.ovpn`, every value is checked for newline, CR, NUL, other control characters, `"` and `\`; on violation nothing is written and the API returns 400 | ## Rules (layer A) | Field | Rule | |---|---| | `port`, `management_port` | 1-65535 | | `vpn_network` + `vpn_netmask` | valid IPv4 network address for a contiguous mask, prefix /8-/30 | | `split_routes[]` | `a.b.c.d/nn` or `a.b.c.d mask`, at most 256 | | `dns_servers[]` | IPv4/IPv6 addresses, at most 8 | | `public_ip` | IP address or hostname | | `management_interface_address` | loopback only | | `tun_mtu`, `mssfix` | 576-9000, 536-1500 | | `connect_script`, `disconnect_script` | empty or `/etc/openvpn/scripts/` | | PKI `fqdn_ca`, `fqdn_server` | `^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$`, no `..` | | PKI `easyrsa_dn` | `cn_only` or `org` | | PKI country / province, city, org, ou / email | `^[A-Z]{2}$` / `^[A-Za-z0-9 .,_-]{0,64}$` / simple email pattern | | PKI `key_size`, days | 2048/3072/4096; 1-36500 | ## Results | Check | Result | |---|---| | Round trip of current server and PKI settings via `PUT` | 200 | | 17 malicious/invalid values (newline in DNS or routes, quote, `../` and `/tmp` scripts, port 0/70000, bad mask, host bits in network, non-loopback management, `a b;c` host, MTU 100, `/` in organisation, lowercase country, `../` in FQDN, key size 512) | all 422 with a clear message | | 4 valid changes (DNS incl. IPv6, routes in both notations, hostname, allowed script path) | 200 | | Script checks: root-owned 755 file / group-writable / symlink out of the directory / missing / outside the directory / traversal / empty | accepted / rejected / rejected / rejected / rejected / rejected / accepted | | Layer C with unsafe values injected past the schema (newline in DNS, quote in route, newline in script, script outside the directory, newline in management address) | all blocked, nothing written | | Regression: settings render to `server.conf` | identical to the live config | Settings were restored after the tests and left unchanged. ## Notes - The scripts directory `/etc/openvpn/scripts/` does not exist by default; create it as `root:root 755` and put root-owned `755` scripts there before enabling `user_defined_cdscripts`. - Next step (planned): run the API as an unprivileged user with a root helper that re-validates the config before installing it. See the project plan.