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.
This commit is contained in:
2026-07-20 00:43:18 +02:00
parent 2ee6aad2b7
commit cff15408bf
2 changed files with 47 additions and 2 deletions
+17 -2
View File
@@ -243,7 +243,7 @@ Audited 2026-07-20 — which public hostnames require credentials:
| Host | Requires credentials? | Notes | | 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` | | `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 `<Directory>` block, intentional | | `dyndns.jayfield.org` | Yes — HTTP Basic Auth | Vhost-level `<Directory>` 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` | N/A — root is `403` (no content served there) | |
| `mail.jayfield.org/rspamd` | Yes — rspamd's own login page | | | `mail.jayfield.org/rspamd` | Yes — rspamd's own login page | |
| `cloud.jayfield.org` (Nextcloud) | Yes — redirects to `/login` | | | `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 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 proxy on its own subdomain, `portainer.jayfield.org`, same pattern as
`web.jayfield.org`/`cloud.jayfield.org`; see "Portainer" above and `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`.)
+30
View File
@@ -3,6 +3,36 @@
Outstanding hardening items (see `README.md` for the full service overview). Outstanding hardening items (see `README.md` for the full service overview).
Nothing here is urgent; all are low-risk, no-downtime changes. 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 - [x] **OS command injection in the dyndns update scripts** — done 2026-07-20
`/var/www/dyndns/nsupdate.php`, `nsupdate_ipv4.php`, and `/var/www/dyndns/nsupdate.php`, `nsupdate_ipv4.php`, and
`nsupdate_ipv6.php` (used by both `index.php` and `nic/update.php`, `nsupdate_ipv6.php` (used by both `index.php` and `nic/update.php`,