<?php
// WHP global PHP-runtime safety net.
//
// PURPOSE
// Restricts every PHP filesystem function (`unlink`, `rmdir`, `scandir`,
// `fopen`, `RecursiveDirectoryIterator`, …) to a strict allow-list of paths
// the panel and its CLI workers legitimately need. The check happens AFTER
// symlink resolution, so a symlink to `/etc` planted in customer-controlled
// content cannot be walked through to wipe host paths.
//
// HISTORY (this file is the kernel-layer answer to all of these):
//   * 2026-05-28 — whp02 binaries wiped from /usr/bin
//                  (CpanelBackupImporter::removeDirectory followed a symlink to /)
//   * 2026-05-29 — whp02 /etc wiped, same vector, same import
//   * 2026-05-29 audit at /tmp/recursive-walker-audit.md surfaced three more
//                  HIGH-severity instances of the same bug class in
//                  site-builder.php, filemanager/index.php, and valkey-mgmt.php
//
// open_basedir is the right layer for this protection because:
//   1. It cannot be bypassed by writing the bug a different way — PHP
//      enforces it inside every filesystem function at the C level.
//   2. It applies to every PHP entry point that loads this file — no per-
//      call-site discipline required.
//   3. Symlinks are resolved BEFORE the check, so traversal via symlink is
//      treated as access to the target path (which is what we want).
//
// LIMITS — what this does NOT protect against:
//   1. `shell_exec`, `system`, `exec`, `passthru` calls. open_basedir does
//      NOT fence shell-outs. Use systemd ProtectSystem= (see
//      configs/whp-cpanel-import@.service) or per-command authorization
//      for that class.
//   2. `docker exec` and Docker API calls. Same caveat.
//   3. Code that runs before this file is loaded (none, by convention —
//      auto-prepend loads us first; CLI scripts that follow the
//      `require_once config.php` convention pick us up there).
//
// COMPATIBILITY
// `ini_set('open_basedir', …)` is idempotent — a second call with the same
// value is a no-op. If a stricter value is already in effect (e.g., set via
// php.ini), the runtime call SILENTLY FAILS and the stricter value wins.
// That's the desired behaviour: this file ratchets in a baseline; ops can
// always tighten further per-host without changing code.

if (function_exists('ini_set')) {
    // Allow-list. Each prefix opens every path beneath it. Resolve symlinks
    // before checking, so /docker/users/<user>/<domain>/symlink-to-/etc
    // resolves to /etc, which is OUTSIDE this list → blocked.
    //
    // When adding a new entry: prefer narrower paths (e.g.,
    // `/etc/letsencrypt` rather than `/etc`). Every entry is a piece of
    // attack surface; the principle is "least authority."
    $whp_open_basedir_allowed = implode(PATH_SEPARATOR, [
        // Customer + WHP working directories
        '/docker',                  // /docker/whp + /docker/users + /docker/mysql + /docker/powerdns ...
        '/tmp',                     // cpanel_import_* tempdirs, session temp dir, etc.
        '/var/log',                 // httpd, php-fpm, whp/, cpanel-import-*.log
        '/var/lib/whp',             // cpanel-imports/, sitesmith/, monitor.db, etc.
        '/var/lib/whp-ai-monitor',  // ai-monitor.php + critical-banner.php read monitor.db here
        '/var/lib/php/sessions',    // PHP session save path on AlmaLinux defaults
        '/run',                     // /run/php-fpm, /run/whp-*.lock
        '/proc',                    // host-info readers (cpu count, mem, /proc/self)
        '/sys/fs/cgroup',           // dashboard.php reads per-container memory.current / memory.stat
        '/var/spool/mail',          // legacy mailbox lookups

        // System config the panel reads — narrow files/dirs only, NOT the
        // full /etc tree. The destruction bug class we're guarding against
        // is "recursive walker descends through a symlink and wipes host
        // paths"; the wormhole-to-everything risk was `/etc` and `/usr/bin`
        // being walkable. With these narrow entries, /usr/bin, /sbin, /lib,
        // /boot, /home, /var/lib (other than /var/lib/whp + sessions),
        // /var/cache, and the rest of /etc remain off-limits — so the
        // destruction-class bug still gets blocked even though specific
        // config files are readable.
        '/etc/whp',                 // encryption.key, ai-monitor.conf, trusted-proxies.conf
        '/etc/letsencrypt',         // certificate renewal flows
        '/etc/whp-version',         // single file (used by update-management.php)
        '/etc/passwd',              // user-lookup reads in usermgmt / mysqlmgmt / delegated_user_manager
        '/etc/hosts',               // server-settings.php reads it
        '/etc/resolv.conf',         // server-settings.php reads it
        '/etc/httpd/conf.d',        // ssl_manager reads ssl.conf
        '/etc/proftpd',             // ssl_manager reads mod_tls.conf
        '/etc/systemd/system',      // cpanel-import-ajax checks for whp-cpanel-import@.service unit

        // Root-owned credentials and WHP install dir on the server
        '/root/.my.cnf',            // MySQL root pw (panel reads via PHP fs functions in some paths)
        '/root/.pgpass',            // PostgreSQL root pw (config.php:17 and adminer-sso.php:22)
        '/root/whp',                // WHP source on server — version file + scripts under it
    ]);

    // Only set if not already set. Calling ini_set with a broader value
    // when a stricter open_basedir is in place returns false; we don't want
    // to spam the error log with that failure.
    if ((string) ini_get('open_basedir') === '') {
        @ini_set('open_basedir', $whp_open_basedir_allowed);
    }

    // open_basedir multiplies the cost of every fs op by a realpath resolve.
    // Bump the cache from PHP's 16K default and extend its TTL so we don't
    // re-walk the same paths every request.
    @ini_set('realpath_cache_size', '512K');
    @ini_set('realpath_cache_ttl',  '600');
}
