fix(security): require WEBHOOK_SECRET for the Nextcloud webhook receiver

GHSA-8vh3-g2qg-2h2c (CVSS 9.1, CWE-306): POST /webhooks/nextcloud had no
authentication when WEBHOOK_SECRET was unset (the default). The receiver
trusted the attacker-supplied user.uid and fed it to Qdrant, letting an
unauthenticated network caller delete or re-index any user's vector
embeddings.

Webhooks now require WEBHOOK_SECRET end-to-end:

- app.py: the /webhooks/nextcloud route is only mounted when WEBHOOK_SECRET
  is set; otherwise it 404s and a startup warning notes vector sync falls
  back to the polling scanner.
- webhook_receiver.py: removed the warn-and-accept fallback. No secret -> 503,
  missing/invalid bearer -> 401; the payload is never processed unauthenticated.
- webhook_routes.py / api/webhooks.py: webhook_auth_pair() raises
  WebhookSecretNotConfigured instead of returning authMethod="none"; both
  registration entry points return a clear 503 so no dead unauthenticated
  webhooks are created.

Also expose webhooks availability to the Astrolabe UI via GET /api/v1/status
("webhooks_enabled": bool), set WEBHOOK_SECRET on the docker-compose
semantic-search dev services, and update env.sample + ADR-010 / ADR-018 /
webhook-management-guide docs.

Vector sync still works without a secret via the polling scanner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-14 18:18:34 +02:00
co-authored by Claude Opus 4.8
parent a818b49b3f
commit 4fc2b10945
16 changed files with 337 additions and 102 deletions
+17 -4
View File
@@ -2321,10 +2321,23 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
# Add Nextcloud webhook receiver (queues DocumentTasks for vector sync).
# Implementation lives in vector/webhook_receiver.py; the handler reads
# the send-stream from request.app.state.document_send_stream.
routes.append(
Route("/webhooks/nextcloud", handle_nextcloud_webhook, methods=["POST"])
)
logger.info("Webhook endpoint enabled: /webhooks/nextcloud")
#
# Security (GHSA-8vh3-g2qg-2h2c): the receiver trusts the attacker-supplied
# user.uid in the payload and feeds it to Qdrant, so an unauthenticated
# POST could delete/re-index any user's embeddings. The route is therefore
# only mounted when WEBHOOK_SECRET is configured; without it the webhook
# feature is off and vector sync still reconciles via the polling scanner.
if settings.webhook_secret:
routes.append(
Route("/webhooks/nextcloud", handle_nextcloud_webhook, methods=["POST"])
)
logger.info("Webhook endpoint enabled: /webhooks/nextcloud")
else:
logger.warning(
"Webhook endpoint disabled: WEBHOOK_SECRET is not set. "
"/webhooks/nextcloud will return 404; vector sync relies on the "
"polling scanner. Set WEBHOOK_SECRET to enable webhook-driven sync."
)
# Add management API endpoints for Nextcloud PHP app
# Tier 1: Public endpoints (no auth required)