fix: exec()/shell_exec() crash on hardened PHP; add Telegram channel adapter - #10
fix: exec()/shell_exec() crash on hardened PHP; add Telegram channel adapter#10rjonesbsink wants to merge 2 commits into
Conversation
disable_functions = shell_exec, exec, system, passthru, popen is a common
hardening default on Windows/IIS PHP installs. PHP's @ suppression does
not catch the resulting "Call to undefined function" fatal error, so
every call below crashed outright — several of them mid-request with
display_errors off, producing a completely empty HTTP response body
(surfaces client-side as "Unexpected end of JSON input" with no clue why).
Replaced each exec()/shell_exec() call with a small proc_open()-based
helper (argv array, no shell, so no escapeshellarg either), guarded by
function_exists('proc_open'). proc_open isn't typically included in
hardening presets that disable exec/shell_exec, so this restores full
functionality instead of just degrading gracefully.
- sql/run_migrations.php: subprocess-per-migration runner
- tools/install_fresh.php: invokes sql/run_migrations.php
- tools/check-schema.php: same pattern, twice (--repair run + the
fresh-process re-check)
- api/health.php: checkOs() uptime probe — user-facing, this is what
broke status.php with the JSON-parse error above. Bonus fix while in
here: wmic doesn't exist at all on Windows 11 24H2+, added a
PowerShell Get-CimInstance fallback formatted to the same
yyyyMMddHHmmss shape wmic used, so the existing parser needed no
changes.
- inc/tts/engine.php: tts_bin_on_path() which/where binary detection
- proxy/ZelloProxyApp.php: binOnPath(), identical pattern
Also adds inc/channels/telegram.php: the Settings -> Telegram panel has
a full config UI (token, chat ID, a "Send Test" button) but no backend.
Every other channel (slack, sms, email, smtp, push, aprs, dmr,
meshtastic, meshcore, local_chat) has an adapter in inc/channels/ that
self-registers via broker_register(), auto-loaded by inc/broker.php.
Telegram had none, so even with the test button wired up (config.js
change below), api/chat.php's test_channel action would reject it with
"Unknown or unregistered channel: telegram" — and real incident/PAR/
system alerts never reached Telegram either, silently. New file mirrors
inc/channels/slack.php's structure (Bot API sendMessage over cURL).
- assets/js/config.js: wires up the missing #btnTestTelegram click
handler in loadTelegramConfig() — compare the #btnTestSlack handler
immediately below it in the same file, which already existed.
Clicking "Send Test" for Telegram previously did nothing at all.
Verified against a live Windows/IIS install with the hardening preset
above: migrations/install/schema-check all run clean, System Status
loads without error, Telegram Send Test delivers a real message.
Based on the tree prior to 1c56498 (the dashboard_tables.sql /
install_fresh.php error-surfacing fix) per your note on openises#5 that you'd
rather resolve any overlap there yourself on merge.
Fixes openises#5
c47977a to
ef10248
Compare
Per @ejosterberg's audit on openises#7: telegram_bot_token matches the _token$ suffix in is_secret_setting_key(), so the server has always masked it (GET settings returns telegram_bot_token_set instead of the value). loadTelegramConfig() didn't know that — it read settings.telegram_bot_token directly (always undefined) and rebuilt the save payload by hand with no "blank means keep" guard, so saving the Telegram panel for any reason (e.g. just updating the chat ID) silently wiped the bot token. This is the same bug as feed_api_key, just via bespoke JS instead of the generic data-key path. Fix: give both Telegram fields data-key (telegram_bot_token gets data-secret="1" too, matching every other credential field), and switch loadTelegramConfig() to the generic applySettingsToForm()/ collectSettingsFromForm() helpers plus the same manual placeholder handling feed_api_key needed (applySettingsToForm doesn't render a "stored" placeholder from the _set sentinel on its own). Verified against tests/test_settings_secret_fields.php from main: telegram_bot_token no longer appears in the failure list. (The other three failures the test still reports on this branch are the pre-existing owm_api_key/geocoding_api_key/etc. gaps and the two boolean-masking issues, already fixed independently on main by 07b9d1d — nothing to do here, they'll merge in on their own.)
|
Added a second commit here per @ejosterberg's note on #7: Rather than bolt on a one-off fix, brought Telegram's two fields onto the same Verified against |
|
Thank you for this — and for the four issue reports that came with it. Root-causing I reviewed this with a security focus and test-merged it against What I checked, and what's good
Also worth noting because it isn't in the PR body: the The one blocker: pipe deadlockIn Measured on Windows: 4 KB of stderr passes, 8 KB hangs indefinitely. This is likely precisely where the PR is aimed. A hardened host running The smallest fix, verified clean up to 1 MB, also matches what the surrounding code already does with 2 => ['redirect', 1] // instead of ['pipe', 'w'] — then drop the stderr readWorth applying the same treatment in Two minor things, not blockers
Happy for you to push the pipe fix to this branch, or say the word and I'll apply it on merge and credit you. Either way this is going in. One question: was the Telegram adapter something you needed, or added for completeness? Asking because it's a new user-facing channel and I'd want a line in the setup docs before it ships. |
Summary
Fixes #5.
disable_functions = shell_exec, exec, system, passthru, popenis a common hardening default on Windows/IIS PHP installs. PHP's@suppression doesn't catch the resulting "Call to undefined function" fatal error, so every call listed below crashed outright — several mid-request withdisplay_errorsoff, producing a completely empty HTTP response body (surfaces client-side asUnexpected end of JSON inputwith zero indication why).Replaced each
exec()/shell_exec()call with a smallproc_open()-based helper (argv array, no shell, so noescapeshellargeither), guarded byfunction_exists('proc_open').proc_openisn't typically included in hardening presets that disableexec/shell_exec, so this restores full functionality rather than just degrading gracefully.Changes
sql/run_migrations.php— subprocess-per-migration runnertools/install_fresh.php— invokessql/run_migrations.phptools/check-schema.php— same pattern, twice (--repairrun + the fresh-process re-check)api/health.php—checkOs()'s uptime probe. This one is user-facing: it brokestatus.php(System Status page) with the JSON-parse error above. Bonus fix while in here:wmicdoesn't exist at all on Windows 11 24H2+ — added a PowerShellGet-CimInstancefallback, formatted to the same 14-digityyyyMMddHHmmssshapewmicused, so the existing parser needed no changes.inc/tts/engine.php—tts_bin_on_path()which/where binary detectionproxy/ZelloProxyApp.php—binOnPath(), identical patternAlso adds
inc/channels/telegram.php: the Settings → Telegram panel has a full config UI (token, chat ID, a "Send Test" button) but no backend at all. Every other channel (slack,sms,email,smtp,push,aprs,dmr,meshtastic,meshcore,local_chat) has an adapter ininc/channels/that self-registers viabroker_register(), auto-loaded byinc/broker.php. Telegram had none, so even with the test button wired up,api/chat.php'stest_channelaction rejected it withUnknown or unregistered channel: telegram— and real incident/PAR/system alerts never reached Telegram either, silently. The new file mirrorsinc/channels/slack.php's structure (Bot APIsendMessageover cURL).assets/js/config.js— wires up the missing#btnTestTelegramclick handler inloadTelegramConfig(). Compare#btnTestSlack's handler immediately below it in the same file, which already existed. Clicking "Send Test" for Telegram previously did nothing at all.About the base commit
This branch is built on the tree just before
1c56498(thedashboard_tables.sqlfix +install_fresh.phperror-surfacing), per your comment on #5 saying you'd rather resolve any overlap yourself on merge rather than have me rebase onto it. Turns out there isn't any real overlap to resolve — your changes landed in the$importSqlFileclosure's catch block, mine touches the migration-invocation step further down — so GitHub reports this as cleanly mergeable as-is.sql/dashboard_tables.sqlitself isn't touched by this PR at all, since your fix already covers it.Testing
Verified against a live Windows/IIS install with the hardening preset above:
php tools/install_fresh.phpandphp sql/run_migrations.phprun clean end-to-endphp tools/check-schema.php --repaircompletes without crashingstatus.php) loads without the JSON-parse error