- Schema validators on the update models (ports, subnet/mask, routes, DNS, public host, loopback-only management address, MTU/MSS, script paths, PKI DN fields, key size and lifetimes). - Script paths must be root-owned, non-writable files directly inside /etc/openvpn/scripts (services/validation.py). - Generators refuse values with newlines, quotes, backslashes or control characters; router maps validation errors to HTTP 400. - Add change record and links. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
3.4 KiB
3.4 KiB
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/<letters, digits, . _ -> |
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 asroot:root 755and put root-owned755scripts there before enablinguser_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.