nebula-mesh: Non-Admin SSRF via Unguarded allow_private Webhook Flag
A low-privilege operator in nebula-mesh can set a single flag on their own webhook subscription to make the server send HTTP requests to loopback and private addresses, bypassing the built-in SSRF gua
The problem
In nebula-mesh versions 0.6.0 through 0.7.1, the webhook subscription endpoints (`POST` and `PATCH /api/v1/webhook-subscriptions`) accept an `allow_private` field from any authenticated operator, including those with the unprivileged `user` role.
When a subscription has `allow_private: true`, the dispatcher swaps the guarded HTTP client for an unguarded one, skipping all loopback, private, and link-local address rejection. Every other admin-level toggle in the codebase gates on `isActiveAdmin`; this field was the only exception.
An attacker can probe internal services, POST event payloads to them, and use delivery status fields (`last_status`, `last_error`) as a reachability oracle.
Proof of concept
A working proof-of-concept for this issue in github.com/forgekeep/nebula-mesh, with the exact payload below.
POST /api/v1/webhook-subscriptions HTTP/1.1
Authorization: Bearer <non-admin-operator-api-key>
Content-Type: application/json
{"url":"http://127.0.0.1:9999/internal-admin","allow_private":true,"events":["host.enrolled"]}
# Server returns 201 Created with allow_private:true persisted.
# Trigger delivery by firing any lifecycle event on a host you own:
POST /api/v1/hosts/<host-id>/block HTTP/1.1
Authorization: Bearer <non-admin-operator-api-key>
# The nebula-mesh server will POST the webhook payload to 127.0.0.1:9999.The root cause is in `internal/api/webhooks.go`: `handleCreateWebhookSubscription` (line 67) and `handleUpdateWebhookSubscription` (line 110) persist the caller-supplied `AllowPrivate` value with no role check. Only resource ownership is loosely checked on updates, and nothing at all guards the create path.
At delivery time, `internal/webhook/webhook.go` lines 294-296 branch on `tgt.AllowPrivate` to select `d.unguarded` over `d.guarded`. The unguarded client skips `config.ValidateWebhookURL`, which is the function that rejects loopback, private, and link-local targets.
The patch adds an `isActiveAdmin` gate on `AllowPrivate` at the API layer, matching the pattern already used by every other privileged field in the codebase (network create, settings PATCH, CA management). CWE-862 (Missing Authorization) is the primary root cause; the SSRF (CWE-918) is the direct consequence.
The fix
Upgrade to nebula-mesh v0.7.2, which adds an admin-role check before persisting `allow_private: true` on both the create and update paths (commit f3c54530e388dd21763e548923426e60a8e93ff0). On existing deployments, audit webhook subscriptions for non-admin-owned rows where `allow_private = true` and reset them before upgrading.
Related research
- high · 8.1CVE-2026-61699CVE-2026-61699: nebula-mesh Certificate Blocklist Never Enforced on Mesh Peers
- high · 8.6CVE-2026-54628CVE-2026-54628: Anyquery SSRF via Unrestricted SQLite Virtual Table Modules in Server Mode
- highCVE-2026-61549CVE-2026-61549: Woodpecker CI Kubernetes Backend Privilege Escalation via serviceAccountName
- high · 7.5CVE-2026-54629CVE-2026-54629: Anyquery Local File Read via Unrestricted SQLite Virtual Table Modules