Skip to content

v1.26.1 — **Security release. Update promptly** — one finding allows

Choose a tag to compare

@Tux82 Tux82 released this 20 Aug 19:25
· 27 commits to main since this release

Security release. Update promptly — one finding allows a hosting tenant to gain root on the host.

Local privilege escalation via .htaccess/.htpasswd (GHSA-mjmx-xpqq-p2h8)

Severity: High (CVSS 8.8). An authenticated hosting tenant — the lowest privilege the panel issues — could obtain arbitrary root file read, write and chown on the host.

The account portal's .htaccess/.htpasswd manager validated the directory of the target path but appended the filename afterwards, unchecked. Because a tenant controls their own document root over SFTP, replacing .htaccess with a symlink made the panel's root cp/cat/chown follow it — reading /etc/shadow, writing /etc/cron.d/, or taking ownership of any root-owned file.

All such access now goes through a single root helper. Callers pass an account and a relative directory, never a path; the helper re-derives the document root from the panel database, re-checks domain ownership, and opens the file with O_NOFOLLOW so the kernel refuses symlinks. Eight broad sudo grants over /home/* were removed and replaced with one narrow grant.

Reported privately. No action is required beyond updating — the fix applies automatically.

phpMyAdmin sign-on credentials were world-readable

The temporary sign-on token file held a plaintext database password — the MySQL root password on the admin route — at mode 0644. It is unlinked when phpMyAdmin picks it up, so the exposure was abandoned hand-offs, which lingered indefinitely and were readable by any local user, including every hosting tenant. Now created 0600 before any content is written, with stale tokens swept.

Hosted sites saw the wrong client IP for every request

Behind Cloudflare Tunnel, cloudflared connects over loopback, so REMOTE_ADDR was 127.0.0.1 for every request on every hosted site. Consequences:

  • per-IP rate limiting in any hosted app was one global bucket
  • access logs could not be used for abuse investigation
  • the panel's own fail2ban jail recorded failed logins as loopback — which jail.local ignores — so it logged every brute-force attempt and could ban none
  • apps compensating by parsing X-Forwarded-For got it wrong, because Cloudflare appends to that header: the left-most entry is attacker-controlled

inetp cf_remoteip now configures mod_remoteip with Cloudflare's published ranges, refreshed weekly. Hosted applications need no changes — REMOTE_ADDR is simply correct.

Sessions were shared across every domain in an account

Session storage was scoped per account rather than per domain, so a session and its CSRF token minted on one domain were valid on every other domain that account owns. For anyone using the multi-domain feature, a staging site or a legacy app with an XSS became a foothold against their more valuable domains.

Sessions are now scoped per domain with a per-domain cookie name, and hosted sites get HttpOnly, Secure, SameSite=Lax and strict session-id mode — none of which PHP sets by default.

This logs users out once. The change applies to a domain when its PHP-FPM pool is next written (adding a domain, or inetp rebuild_pools). Existing sessions for that domain end at that point. Nothing else is affected.

PHP session files were never deleted

The panel's per-pool session path is invisible to Debian's session cleaner, which reads only the system php.ini. PHP's own collector is disabled on Debian by design. Nothing removed them: one account reached roughly 4.2 million files, with the directory entry alone at 281 MB — a cost paid on every session lookup.

inetp session_reaper now runs hourly, honours session.gc_maxlifetime, and works in bounded batches with nice/ionice, because an unbounded delete over millions of files can stall a container.

Run inetp session_reaper --dry-run after updating to see what has accumulated. Note that ext4 does not shrink a directory entry once grown — reclaiming that space requires recreating the directory, which the dry run flags.

Per-site logs were never rotated

Nothing in /etc/logrotate.d/ matched /home, so hosted sites' Apache logs grew without bound — 218 MB and 106 MB access logs were found unrotated since April — at mode 0644. Access logs record full query strings, so any application placing a token in a URL leaked it into a world-readable file.

Ships /etc/logrotate.d/inetpanel-sites (weekly, 8 kept, copytruncate, mode 0640), and tightens existing world-readable site logs on update.

Upgrading

inetp panel_update, or wait for the nightly auto-update. Everything except the session change is transparent.

With thanks to the security researcher who reported the privilege escalation privately, and to the operator whose fleet audit surfaced the rest.