Forums Bug Reports Thread

SSRF: bridge webhook targets bypass SsrfGuard (only an https:// prefix check), worker fetches the URL server-side

Patrick Bass · Jun 6 · 11 · 1 Locked
[Major] [High Priority] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 7:37pm

Area: Admin plane (audit p12) · Surface: POST /admin/bridge, POST /admin/bridge/{id}, POST /admin/bridge/{id}/test (AdminBridgeController) + bin/process-bridge-queue.php · Dimension: security · Severity: major

A tenant admin (role>=4) can register a Slack/Teams 'bridge target' whose webhook URL points at an internal address (e.g. https://169.254.169.254/latest/meta-data/, https://10.x/, https://127.0.0.1:.../). The bridge worker fires a server-side POST to that URL. The generic /admin/webhooks endpoint blocks exactly this via SsrfGuard, so the bridge path is an inconsistent gap rather than an accepted risk. OWASP A10:2021 Server-Side Request Forgery. Mitigations that cap severity: the request is POST-only with a fixed JSON card body, response body is truncated to 400 bytes and not surfaced to the admin (only delivery status is shown), CURLOPT_FOLLOWLOCATION=>false (process-bridge-queue.php:200) blocks redirect pivots, and the action requires a valid CSRF token + role 4. Still reachable for internal port-scanning / hitting unauthenticated internal services / cloud metadata reachability probing.

Evidence

AdminBridgeController::validateFields() (platform/src/Controllers/AdminBridgeController.php:318-333) is the only URL check and it is just a scheme prefix test: `if (!preg_match('#^https://#i', $url)) { return 'Webhook URL must start with https://'; }`. No host/IP validation. The admin-supplied URL is then encrypted (create():105 `BridgeCipher::encrypt($url)`) and, in the worker, decrypted and fetched server-side: bin/process-bridge-queue.php:162 `$url = BridgeCipher::decrypt(...)`, :189 `$endpoint = $adapter->endpoint($url)`, :192 `$ch = curl_init($endpoint); ... curl_exec($ch);`. Contrast the generic webhook system, which DOES guard: Models/WebhookEndpoint.php create() calls `SsrfGuard::assertSafePublicHttps($url)` (the guard blocks RFC1918 10/8 172.16/12 192.168/16, link-local 169.254/16, and the cloud metadata IP 169.254.169.254 per Services/SsrfGuard.php:17-23). `grep -rln SsrfGuard` returns only Models/WebhookEndpoint.php and Services/SsrfGuard.php — AdminBridgeController and the bridge worker never call it.

Suggested fix. Call SsrfGuard::assertSafePublicHttps($url) inside AdminBridgeController::validateFields() (and at decrypt-time in process-bridge-queue.php before curl, since DNS can rebind between save and delivery — resolve+pin or re-check the resolved IP). Reuse the existing SsrfGuard rather than duplicating the prefix check.

Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.


Patrick Bass
@mobieus

🚀 Jun 7, 2026 5:25am

Resolved — fixed and deployed. Commit dd336ac47616, shipped dev-first then to all tenants on 2026-06-06.

Added SsrfGuard::assertSafePublicHttps() to AdminBridgeController: invoked inside validateFields() (covers the create() save path) and again in update() before encrypting a rotated webhook URL, catching SsrfException and surfacing the message to the admin. Imported App\Services\SsrfGuard + SsrfException and reused the existing guard rather than duplicating logic. php -l passes. Note: the DNS-rebinding re-check at decrypt-time in bin/process-bridge-queue.php is in a different file I do not own, so only the save-time/registration guard could be applied here.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.