Repository navigation
fix(php): a package that allows PHP exec functions really allows them, and picks disabled functions one by one — GH #1701 - #1990
Merged
Conversation
…, and can pick functions one by one — GH #1701 A hosting package's "Allow PHP exec functions" (GH #402) removed the pool's disable_functions line, but PHP Defense (Snuffleupagus) in enforce mode bans system, exec, shell_exec, passthru, popen, proc_open and pcntl_exec in every PHP-FPM master on its own. So on a box in enforce mode the opt-out did nothing: shell_exec still aborted. Reproduced on the test box: with the opt-out on and PHP Defense in enforce, function_exists('shell_exec') is true, disable_functions is empty, and the call is still dropped ("[disabled_function][drop]"). Disabled functions per package: - hosting_packages.php_disabled_functions (migration 000314, TEXT NULL) holds the disable_functions list the package's pools run with. NULL = the GH #401 lockdown. An admin can allow any lockdown function back or disable more. Names are lowercased, de-duplicated and checked against ^[a-z_][a-z0-9_]{0,63}$ (at most 200). - php_exec_enabled is now derived from the list (true when no lockdown function is disabled) and written with it, so an older binary reading the row keeps the right opt-out. A NULL list reads through php_exec_enabled, so existing opt-outs keep working with no backfill. - REST: php_disabled_functions on create and update (string sets, null resets, omitted leaves it). It wins over php_exec_enabled; the old toggle still works and keeps other entries. CLI: --php-disabled-functions <list>|default|none on package create and edit, same rules. A change to the effective list re-renders the package's pools (REST) or marks them pending (CLI), as the toggle did. - The two pool-apply callers send the list through one helper. The cPanel importer's direct apply is left to the next reconcile, as its comment already says. PHP Defense per pool (agent): - A pool whose disable_functions leaves any of those seven functions enabled gets /etc/jabali/snuffleupagus/pools/<slug>.rules: a copy of active.rules with only those flat bans commented out (enforce or simulation form; filtered rules and the eval blacklist stay), and an ini in its own scan dir (/etc/php/<ver>/jabali-ext/<slug>/ 90-jabali-php-defense.ini) pointing sp.configuration_file at it. fpm-exec already scans that dir after conf.d, so it applies to that master only. Checked on the test box in enforce: shell_exec runs through the copy, exec (still banned in it) aborts, and without the ini both abort; a USR2 reload is enough. - The agent derives the lift from the disable_functions it renders, so the two cannot disagree, and only ever lifts those seven. - snuffleupagus.apply_rules rebuilds every copy from the new rules before it reloads the pools, and removes copies whose pool is gone. Pool remove and the orphan reaper delete them too. The agent now refuses a disable_functions value that is not comma-separated lowercase function names. - CLI PHP (cron, SSH) keeps active.rules through cli.ini. Tests: model normalisation, effective list, legacy rows, derived flag and pool params; REST create/update precedence, null reset, fan-out only on a real change; repo allowlist; CLI create/edit; agent rule filter (enforce + simulation lines, nothing outside the seven), apply/ version move/removal, regeneration with an orphan and a foreign file, name validation, and the order of the wiring. The agent and panel lists are pinned to each other. Claude-Session: https://claude.ai/code/session_0173PcNd4h6NceYPc4FuhvXj
…e what a domain's PHP pool really runs with — GH #1701 Package editor: "Disabled PHP functions" replaces the "Allow PHP exec functions" switch. The ten command-execution functions are checkboxes (checked = disabled, all checked by default), with an "Also disable" list for any other function and a reset to the default. It says which ones also lift the PHP Defense ban. The editor sends the list (null at the default) and no longer sends php_exec_enabled; a package saved with the old switch on loads with every box unchecked. The package list's "PHP exec" column shows "locked", "exec allowed" or "N exec allowed", plus extra disabled functions, so a package that re-allows one function no longer reads as fully locked. PHP Settings (admin and tenant): a read-only, collapsed "Disabled functions and paths" section, fetched only when opened, from the new GET /domains/:id/php-settings/effective (owner or admin, cached 15 s per pool). The agent's new php.pool.effective reads the files the pool's master loads: - disabled functions from the pool conf and from the FPM php.ini + conf.d + the pool's scan dir (FPM adds the pool's list to php.ini's), each marked as the hosting package's or server-wide; - what PHP Defense bans there, from the rules file the pool is pointed at (its own copy or active.rules): blocked in enforce, logged in simulation, nothing when off or not loaded for that PHP version; - include_path and session.save_path, from the pool conf (an admin's pool ini override) or php.ini. The table lists the command-execution functions plus anything disabled or banned, with the status a call really meets. Tests: agent read (sources, admin_value over value, pool copy, a pointer outside the copy dir ignored, no module, failed php.ini read), argument checks; endpoint (owner, other tenant refused without an agent call, admin, cache, versioned slug); vitest for the codec, summary, table precedence, the section and the editor save. Claude-Session: https://claude.ai/code/session_0173PcNd4h6NceYPc4FuhvXj
…and the read-only PHP environment — GH #1701 - Hosting packages: the php_disabled_functions field, its default, the derived php_exec_enabled and the CLI flag. - PHP: how the package list reaches the pool and lifts PHP Defense's command-execution bans, and what the read-only section shows. - PHP Defense: packages that allow command execution get their own copy of the rules; CLI PHP keeps the server-wide rules. - User PHP Settings: the read-only "Disabled functions and paths". - ADR-0146: the opt-out is now a list and holds in enforce mode. Claude-Session: https://claude.ai/code/session_0173PcNd4h6NceYPc4FuhvXj
…the domain view shows the pool's real setup — GH #1701 Playwright, mocked API, real browser: - admin unchecks shell_exec and adds mail on a package: the PATCH body carries php_disabled_functions in canonical order and no php_exec_enabled. - tenant opens "Disabled functions and paths": the effective endpoint is read only when the section opens; per-function statuses (allowed, package, php.ini) and the include_path / session.save_path sources render. Falsified: both tests fail against a build whose encoder keeps shell_exec and whose query fires while collapsed. Claude-Session: https://claude.ai/code/session_0173PcNd4h6NceYPc4FuhvXj
…PHP pool fails to reload — GH #1701 snuffleupagus.apply_rules returned only the reload error when both the FPM reload and the per-pool rules rebuild failed. A pool unit that fails to reload is a standing condition on some boxes (a leftover unit of a deleted user), so every rebuild failure was hidden behind it, and a pool's stale copy kept the old PHP Defense mode with no signal. Both errors are now reported. Claude-Session: https://claude.ai/code/session_0173PcNd4h6NceYPc4FuhvXj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GH #1701: the reporter's follow-up (comment 5944502718). It covers one bug and two requests.
Bug: the package's exec opt-out did not enable
shell_execThe cause is PHP Defense (Snuffleupagus). Its
00-base.rulesbanssystem,exec,shell_exec,passthru,popen,proc_openandpcntl_execoutright. In enforce mode that ban overrode the package opt-out: the pool'sdisable_functionsline was gone, but the call still aborted. Reproduced on the test box.The fix: a pool whose package leaves any of those seven enabled now loads its own copy of
active.rules, without the bans on the allowed functions./etc/php/<ver>/jabali-ext/<slug>/90-jabali-php-defense.ini) points that one FPM master at the copy.fpm-execalready adds that dir after the FPMconf.d.phpinfo,assertand the.php-write block. Only flat.drop()/.drop().simulation()lines are ever lifted.cli.ini.snuffleupagus.apply_rulesnow reports a failed copy rebuild even when a PHP pool also fails to reload. Before, the reload error hid it, and a stale copy kept the old mode.Existing opt-out packages need no operator action.
jabali updatealready runsjabali php pool reapply-all(update.go, GH #401 step), which re-applies every pool and writes the copies.Disabled PHP functions, per function
The admin picks the package's list one function at a time.
hosting_packages.php_disabled_functions(TEXT NULL, migration 000314, no backfill).php_exec_enabled.php_exec_enabledis now derived from the list and written with it, so older readers stay correct. When both are sent, the list wins.^[a-z_][a-z0-9_]{0,63}$, at most 200. The agent re-validates them before they reach the pool conf.php_disabled_functionson package create and update. On update,nullresets to the default and an omitted field leaves the list alone. The fan-out to the package's pools runs only when the effective list changes.jabali package create|edit --php-disabled-functions default|none|<list>.--php-execmaps onto the list.The package list column shows a summary.
What a domain's PHP pool really runs with (read-only)
A new collapsed "Disabled functions and paths" section on the domain's PHP Settings page. The admin and the domain owner both see it.
php.pool.effectivereads the files the pool's master loads: the pool conf, plus the FPM php.ini,conf.dand the pool's scan dir.GET /domains/:id/php-settings/effective. It is owner-or-admin, cached for 15 s, and fetched only when the section opens.include_pathandsession.save_pathshow with their source: the pool setting or the server php.ini.Verification
Go: panel-api and panel-agent full suites pass.
UI: vitest passes. New Playwright spec
php-disabled-functions.spec.tsruns in a real browser:php_exec_enabled.Test box (.60), PHP 8.4, PHP Defense in enforce, probes over FastCGI. I installed the binaries by hand and ran
reapply-allasjabali updatewould.php_exec_enabled=1, list NULL)shell_execandexecrunphpinfo)shell_execallowed, plusMAILphp_exec_enabledderived 0; copy lifts onlyshell_exec;shell_execruns,execundefined,mailundefinedphp.pool.effectiveRPCshell_execabsent; paths from php.ini;../etcslug refused.simulation()form, lift kept; RPC reportslogged--php-disabled-functions defaultshell_execundefined after the reload.60 was restored afterwards: original binaries, schema 308, PHP Defense back to simulation, drill files moved to
/root/.trash.https://claude.ai/code/session_0173PcNd4h6NceYPc4FuhvXj