From cff15408bf136cbba5f14702afdfba9d049eadf1 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Mon, 20 Jul 2026 00:43:18 +0200 Subject: [PATCH] Document dyndns command-injection fix and two remaining follow-ups README's public-access table and "previously resolved" summary now reflect the dyndns injection fix and the TSIG-permission regression it uncovered. Adds two open TODO items found during that audit: the dyndns app's lack of real per-user auth binding, and the fact that access logging for every real vhost on this host is currently a no-op. --- README.md | 19 +++++++++++++++++-- TODO.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b73dd9b..4b8855b 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ Audited 2026-07-20 — which public hostnames require credentials: | Host | Requires credentials? | Notes | |---|---|---| | `jayfield.org` / `www.jayfield.org` | No (intentionally public static site) | Was accidentally 401'ing every visitor via a stray `.htaccess` — fixed, see `TODO.md` | -| `dyndns.jayfield.org` | Yes — HTTP Basic Auth | Vhost-level `` block, intentional | +| `dyndns.jayfield.org` | Yes — HTTP Basic Auth | Vhost-level `` block, intentional. Had an OS command injection in its update scripts (fixed 2026-07-20, see `TODO.md`); the app itself still has no real per-user auth binding (open `TODO.md` item) | | `mail.jayfield.org` | N/A — root is `403` (no content served there) | | | `mail.jayfield.org/rspamd` | Yes — rspamd's own login page | | | `cloud.jayfield.org` (Nextcloud) | Yes — redirects to `/login` | | @@ -364,4 +364,19 @@ not a MITM, a self-inflicted interaction between two otherwise-correct pieces of config. Fixed 2026-07-20 by moving it behind an Apache reverse proxy on its own subdomain, `portainer.jayfield.org`, same pattern as `web.jayfield.org`/`cloud.jayfield.org`; see "Portainer" above and -`TODO.md` for the full details.) +`TODO.md` for the full details. +Also: an audit of which public hostnames require credentials (prompted by +the Portainer/HSTS work above) turned up an OS command injection in the +dyndns update scripts (`/var/www/dyndns/nsupdate*.php` built a shell +command by directly interpolating unsanitized `$_GET` input into +`exec()`) — gated by the vhost's Basic Auth, so not anonymously +exploitable, but a real privilege escalation for anyone with valid dyndns +credentials. Fixed 2026-07-20 by rewriting the update path to use +`proc_open()` with array-form arguments (no shell involved at all) plus +strict hostname/IP validation. That same audit also surfaced (a) a +regression from the 2026-07-19 TSIG key hardening that had silently +broken legitimate dyndns updates — fixed with a POSIX ACL scoped to just +that key file, not group membership, since the `bind` group also owns +`rndc.key` — and (b) that access logging for every real vhost on this +host is currently a no-op, and (c) that the dyndns app has no real +per-user auth binding. (b) and (c) are left open — see `TODO.md`.) diff --git a/TODO.md b/TODO.md index 66483cd..dd0d113 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,36 @@ Outstanding hardening items (see `README.md` for the full service overview). Nothing here is urgent; all are low-risk, no-downtime changes. +- [ ] **dyndns app has no real per-user auth binding** — found 2026-07-20, + while fixing the command injection below. + `index.php`'s `pass == '_NO_PASS_'` check is a hardcoded magic + string, not a real per-user credential — there's nothing tying the + single shared Apache Basic Auth identity to *which* hostname label + gets updated. Any caller with valid dyndns credentials can update + any of the zone's 5 hostnames (`jens`, `kack`, `test`, `uschi`, + `vpn`), not just their own. Low severity today since apparently only + one person (`jens`) has credentials, but worth fixing before adding + any second dyndns user. Fix: either move to per-host TSIG-style + tokens, or add a static host-allowlist per `.htpasswd` user in the + PHP. + +- [ ] **dyndns/default-vhost access logging is a no-op** — found + 2026-07-20, while investigating the command injection below. + `dyndns.jayfield.org`, `jayfield.org`, `www.jayfield.org`, + `mail.jayfield.org`, `web.jayfield.org`, and `cloud.jayfield.org` + have no `CustomLog` directive of their own, so requests to them + should fall back to the global `CustomLog ${APACHE_LOG_DIR}/access.log` + in `apache2.conf` — but that file had 0 lines when checked. Only + `default-ssl.conf`/`000-default.conf` explicitly set `CustomLog`. + Net effect: there's currently no access-log trail for any real + vhost on this host, which is exactly the evidence that would be + needed to rule out past abuse of things like the dyndns injection + above. Needs root-causing (global `CustomLog` not actually wired up + to these vhosts? logs going somewhere else? log rotation wiping it + right before checks?) and likely a per-vhost `CustomLog` added to + each site, matching the `default-ssl.conf`/`000-default.conf` + pattern. + - [x] **OS command injection in the dyndns update scripts** — done 2026-07-20 `/var/www/dyndns/nsupdate.php`, `nsupdate_ipv4.php`, and `nsupdate_ipv6.php` (used by both `index.php` and `nic/update.php`,