feat: Telegram channel adapter, test button, and setup guide - #10
Conversation
c47977a to
ef10248
Compare
|
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. |
|
Thanks for the thorough review — especially for measuring the deadlock threshold rather than just flagging the shape. You're right on every point, and I've pushed all of it. The blocker: fixed, and I reproduced your numbers exactlyYour diagnosis was correct and it was a regression I introduced. On this box (Windows/IIS, PHP 8.4.22,
I also ran it end-to-end through the real runner rather than only a synthetic harness: dropped in a temporary migration that writes 74 KB to stderr, then ran
For the three lower-risk ones I went with the null device rather than a redirect — While in there: the three remaining
|
|
Filed the test-suite observation as #13. One correction while tracing it for the writeup: I said above that the |
Review outcome: nothing dangerous, four changes wanted before merge. The merge decision is Eric's.Full adversarial pre-merge security review done against head The Your second round of fixes landed properly. The pipe-deadlock fix is right, including the judgement inside it: No SSRF in the Telegram adapter. The host is a hard-coded literal and the only interpolated component is the token, which lands in the path, after the authority — a The four changes1. Pin
|
| Part | State in the dev tree |
|---|---|
proc_open conversion, all six files |
already there (8a9ec2a), independently |
wmic → Get-CimInstance fallback for Windows 11 24H2 |
already there |
MySQL 8.0 TEXT DEFAULT |
already fixed before that |
telegram_bot_token silent-blanking (550980c) |
fixed generally — applySettingsToForm() now handles every data-secret field, plus a server-side backstop in api/config-admin.php |
inc/channels/telegram.php + the #btnTestTelegram handler |
not in the tree. Still genuinely wanted. |
There is also now a suite gate, tests/test_no_shell_command_execution.php, which fails if a string-form proc_open() appears, if any argv-literal element contains a superglobal or a variable concatenation, or if exec/shell_exec/system/passthru/popen/backticks reappear in those six files. Your conversion is what that exists to protect.
I would rather tell you now than let you spend an evening reconciling eleven files. The Telegram adapter is the part with nothing behind it, and it is the part worth your time. A scope reduction to inc/channels/telegram.php + the config.js handler + the setup doc, with the four changes above, would be a clean thing to land. Entirely your call, and Eric's on the merge either way.
One consequence to be aware of, since it is invisible from this side: this repository is a one-way, full-tree-replace snapshot of a private dev tree. A change merged only here is overwritten by the dev tree's version of that file at the next release — or deleted outright, if the dev tree has no such file. Git raises no objection and no diff shows it. There is now a guard that refuses to publish when that would happen (tools/release-divergence-check.php, which compares the staged snapshot against this repo's main and against the tree the last release published, and fails closed if it cannot reach either). But the flow it enforces is that an accepted contribution gets applied in the dev tree; merging here is the acknowledgement, not the shipping. That is why "already in the dev tree" above means shipped — and why your branch showing as behind is not anyone rejecting it.
For the record, reviewing this turned up two pre-existing defects in the code paths it touches — three settings panels silently wiping stored credentials on save, and CLI-only scripts being reachable over HTTP. Both are fixed (1923ba5, 50fcc74). Neither was yours; they were found because reviewing your work meant reading that code carefully. Details are in my comment on #9 and in #13.
Scope reduced per review: the proc_open conversion, the wmic fallback, the MySQL 8.0 TEXT DEFAULT fix and the telegram_bot_token blanking are all in the dev tree already (8a9ec2a and others), so this branch is now only the part with nothing behind it — the Telegram channel itself, rebased onto current main. The Settings panel has shipped a complete Telegram config UI, including a "Send Test" button, with no backend: no inc/channels/telegram.php, and nothing bound to #btnTestTelegram. Routed sends to the channel would be rejected by broker_send() as unregistered, silently, and the test button did nothing at all. Adds: - inc/channels/telegram.php — Bot API sendMessage over cURL, registered through broker_register() and picked up by inc/broker.php's glob. - The missing #btnTestTelegram handler in config.js, mirroring #btnTestSlack. Deliberately does not touch loadTelegramConfig()'s save path — the data-secret blanking is fixed generally in the dev tree and changing it here would collide with that. - docs/TELEGRAM-SETUP-GUIDE.md, modelled on ZELLO-SETUP-GUIDE.md. - telegram rows in the channel tables in MESSAGE-ROUTING-GUIDE.md and ROUTING-ENGINE-REFERENCE.md. The four review changes: 1. The destination chat is read from configuration only. The $message['telegram_chat_id'] override is gone. inc/router.php forwards a matched message array wholesale (_router_transform() rewrites body/priority/type and leaves other keys intact) and two receive handlers return raw third-party JSON into that path (_slack_receive -> $data['messages'], _sms_receive -> $data['threads']). No provider currently permits an arbitrary top-level key, so it was unreachable — but that is a property of someone else's response schema, not of this codebase. Verified by test: with an invalid configured chat id and a valid override supplied, the send fails closed and transmits nothing. 2. cURL security options stated explicitly rather than inherited — VERIFYPEER, VERIFYHOST 2, no redirects, HTTPS-only for both protocols and redirects, CONNECTTIMEOUT 5. Matches inc/webhooks.php and api/dmr-lookup.php. The existing TIMEOUT 10 is kept and noted in a comment as synchronous inside broker_send(). 3. Token and chat-id format validation, failing closed with an actionable message instead of an opaque Telegram 404. _telegram_status() applies the same checks, so malformed credentials report not_configured rather than a "configured" state that cannot send. 4. The setup guide records the two non-guessable facts: the chat id must be the group's (negative) — a positive DM id fails with "Forbidden: bot can't initiate conversation with a user", which reads as a permissions problem rather than a wrong id — and the bot must be a member of the group before getUpdates shows the chat at all. Verified against a live bot and group on Windows/IIS, PHP 8.4, MySQL 8.0: adapter registers via the broker glob, Send Test delivers, and malformed token / chat id are both rejected before any request is made.
84d7b4a to
f0cfeb4
Compare
|
All four changes made, and the scope reduction taken. Force-pushed — the branch is now five files instead of eleven, rebased onto current Thank you for the heads-up on the dev tree. Reconciling eleven files against work that was already done would have been a genuinely wasted evening, and it is not something I could have discovered from this side. The four1 — chat id pinned to config. Done, and I verified it rather than just making the edit: with an invalid configured chat id and a valid override supplied, the send fails closed on validation and transmits nothing. If the override were still honoured it would have gone through. Your reasoning is recorded in a comment at the call site — the router forwarding the array wholesale, the two receive handlers returning raw third-party JSON, and specifically the point that its unreachability is a property of someone else's response schema rather than of this codebase. That last part is the bit a future reader needs and would not otherwise reconstruct. 2 — cURL options explicit. All six, matching 3 — validation. Both patterns as suggested, failing closed with a message that names what is wrong. I also applied the same checks in 4 — setup guide. One deliberate omissionI left On the snapshot modelUnderstood, and useful to know — I had assumed a merge here was the shipping mechanism. The one practical consequence I would flag: the Happy to fold in anything else. And no objection at all if the answer to the per-message destination idea is "not without the allowlist and the trust marker" — that was your point and I think it is the right one. |
|
Merged. Thank you — this is a genuinely useful contribution, and the way you handled the review made it easy to land. What landed
Reducing the scope to just the Telegram work once the rest had landed in the dev tree was the right call and made this reviewable on its own. The four security changes, and why each was neededThree of these you applied on the branch after the review; the fourth was already in the dev tree. Recording them here so the reasoning is on the record with the code. 1. The destination chat id is pinned to configuration — The original line was Nothing could reach it. Every It was a live hazard. The reason a latent issue was treated as blocking is that this project has been here twice already:
2. cURL security options stated explicitly —
The defaults were already safe, so this changed no behaviour on a normal host. It matters for two reasons: a host with unusual 3. Token and chat-id format validation, failing closed — Not a security fix so much as a diagnosability one — there was no SSRF here, since the host is a hard-coded literal and the token lands in the path, after the authority, where it cannot re-point the request. But a token pasted with trailing whitespace produced an opaque 404 from Telegram, which reads like a permissions problem. Failing closed with a message naming the actual fault is better. Applying the same checks in 4. A durable gate on the argv-array property — The review asked for this to land in the same merge as your Your adapter is in the dev tree, so it survives releasesThis is worth knowing if you contribute again. So merging alone would not have kept your work. Your commit is now in the dev tree with your authorship preserved ( Reintroducing the vulnerable line into the adapter takes that file to 25 passed / 5 failed. So if anyone ever "simplifies" the pin away, they will hear about it. Thanks again — for the adapter, for the setup guide (the two non-guessable facts about the negative group id and bot membership are exactly the things that cost people an afternoon), and for turning the review around as quickly as you did. |
Summary
Adds the Telegram channel adapter — the backend behind a Settings panel that has been shipping a complete configuration UI, including a "Send Test" button, with nothing behind it.
Scope reduced per @ejosterberg's review. The
proc_openconversion, thewmic→Get-CimInstancefallback, the MySQL 8.0TEXT DEFAULTfix and thetelegram_bot_tokenblanking are all in the dev tree already (8a9ec2aand others), so this branch is now only the part with nothing behind it, rebased onto currentmain. Eleven files became five.What was broken
inc/channels/telegram.php, sobroker_send('telegram', …)was rejected as an unregistered channel — silently, for routed incident/PAR/system alerts.#btnTestTelegram, so the shipped "Send Test" button did nothing at all.Changes
inc/channels/telegram.php— Bot APIsendMessageover cURL, registered viabroker_register()and picked up byinc/broker.php's glob.assets/js/config.js— the missing#btnTestTelegramhandler, mirroring#btnTestSlack. Deliberately does not touchloadTelegramConfig()'s save path: thedata-secretblanking is fixed generally in the dev tree and changing it here would collide.docs/TELEGRAM-SETUP-GUIDE.md— modelled onZELLO-SETUP-GUIDE.md.MESSAGE-ROUTING-GUIDE.mdandROUTING-ENGINE-REFERENCE.md.The four review items
1 — Destination pinned to configuration. The
$message['telegram_chat_id']override is gone; the chat id is read from config only.Verified rather than asserted: with an invalid configured chat id and a valid override supplied, the send fails closed on validation and transmits nothing. If the override were still honoured it would have proceeded.
The reasoning is recorded in a comment at the call site, including why it was unreachable (no provider currently permits an arbitrary top-level key) and why that is not a guarantee this codebase owns.
2 — cURL options stated explicitly.
VERIFYPEER,VERIFYHOST 2, no redirects, HTTPS-only for both protocols and redirects,CONNECTTIMEOUT 5. Matchesinc/webhooks.phpandapi/dmr-lookup.php. ExistingTIMEOUT 10kept, with the synchronous-inside-broker_send()consequence noted in a comment.3 — Format validation, failing closed. Token against
/^\d+:[A-Za-z0-9_-]{20,}$/, chat id against/^-?\d{1,20}$/, both with actionable messages instead of an opaque Telegram 404._telegram_status()applies the same checks, so malformed credentials reportnot_configuredrather than a "configured" state that cannot send.4 — Setup guide. Records both non-guessable facts: the chat id must be the group's negative id (a positive DM id fails with
Forbidden: bot can't initiate conversation with a user, which reads as a permissions problem rather than a wrong id), and the bot must already be a member of the group beforegetUpdatesshows the chat.Testing
Verified against a live bot and group on Windows/IIS, PHP 8.4.22, MySQL 8.0:
_telegram_status()reportsconfigured.