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:
co-authored by
Claude Opus 4.8
parent
a818b49b3f
commit
4fc2b10945
@@ -249,16 +249,24 @@ This design keeps concerns separated: webhooks and scanner are independent produ
|
||||
|
||||
### Configuration
|
||||
|
||||
A new optional environment variable controls webhook authentication:
|
||||
A **required** environment variable controls webhook authentication:
|
||||
|
||||
```bash
|
||||
# Optional: Shared secret for webhook authentication
|
||||
# If set, webhooks must include "Authorization: Bearer <secret>" header
|
||||
# If unset, no authentication is required (useful for local development)
|
||||
# REQUIRED for webhooks: shared secret for webhook authentication.
|
||||
# Webhooks must include "Authorization: Bearer <secret>" header.
|
||||
WEBHOOK_SECRET=<generate-random-secret>
|
||||
```
|
||||
|
||||
The webhook endpoint is automatically available at `/webhooks/nextcloud` when the MCP server starts. No feature flags or additional configuration needed—if Nextcloud sends webhooks to this endpoint, they will be processed.
|
||||
> **Security (GHSA-8vh3-g2qg-2h2c).** The receiver trusts the `user.uid` in the
|
||||
> payload and feeds it to Qdrant, so an unauthenticated POST could delete or
|
||||
> re-index any user's embeddings. `WEBHOOK_SECRET` is therefore mandatory:
|
||||
> when it is **unset**, the `/webhooks/nextcloud` route is **not mounted** (it
|
||||
> returns 404) and the receiver refuses any request that reaches it (503).
|
||||
> Webhook registration likewise refuses to create unauthenticated deliveries.
|
||||
> Vector sync still works in this state via the polling scanner — webhooks are
|
||||
> simply disabled until a secret is configured.
|
||||
|
||||
The webhook endpoint is available at `/webhooks/nextcloud` only when `WEBHOOK_SECRET` is set. When configured, Nextcloud must forward the `Authorization: Bearer <secret>` header (registration injects it automatically) on every delivery; requests without a valid header are rejected with 401.
|
||||
|
||||
**Reducing Polling Frequency**: Administrators who configure webhooks may want to reduce polling frequency to minimize API load while maintaining safety reconciliation scans:
|
||||
|
||||
@@ -296,11 +304,13 @@ Administrators who want to enable webhooks:
|
||||
- Endpoint: `https://<mcp-server-host>:<port>/webhooks/nextcloud`
|
||||
- Events: File created/updated/deleted, Calendar object events, Table row events
|
||||
- Filters: Exclude non-content files (images, videos), system directories
|
||||
- Optional: Configure `Authorization: Bearer <WEBHOOK_SECRET>` header
|
||||
- Required: Configure `Authorization: Bearer <WEBHOOK_SECRET>` header (the
|
||||
MCP server's registration endpoints inject this automatically once
|
||||
`WEBHOOK_SECRET` is set)
|
||||
3. **Optionally reduce scanner frequency**: Set `VECTOR_SYNC_SCAN_INTERVAL=86400` (24 hours)
|
||||
4. **Set up webhook workers** (optional): Configure dedicated background job workers for low-latency delivery
|
||||
|
||||
Existing deployments continue using polling without any changes. Webhooks are purely additive.
|
||||
Deployments without `WEBHOOK_SECRET` continue using polling without any changes — the webhook route is simply not mounted. Webhooks are additive but require a configured secret (GHSA-8vh3-g2qg-2h2c).
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -346,7 +356,7 @@ Logs include:
|
||||
|
||||
### Security Considerations
|
||||
|
||||
**Optional Authentication**: When `WEBHOOK_SECRET` is configured, webhook requests must include `Authorization: Bearer <WEBHOOK_SECRET>` header. The server validates this before processing to prevent unauthorized document queueing. For local development, authentication can be disabled by leaving `WEBHOOK_SECRET` unset.
|
||||
**Required Authentication (GHSA-8vh3-g2qg-2h2c)**: The receiver trusts the `user.uid` in the payload and feeds it to Qdrant, so unauthenticated access would let any network caller delete or re-index other users' embeddings. `WEBHOOK_SECRET` is therefore mandatory for webhooks: when it is unset the `/webhooks/nextcloud` route is not mounted (404) and the handler refuses any request that reaches it (503). When set, every request must include `Authorization: Bearer <WEBHOOK_SECRET>`; the server validates it before any processing and rejects mismatches with 401. There is no unauthenticated mode — local development that needs webhooks must also set a secret.
|
||||
|
||||
**Payload Validation**: Webhook payloads are parsed and validated against expected schemas. Malformed payloads are rejected with 400 Bad Request responses.
|
||||
|
||||
@@ -375,7 +385,7 @@ async def test_webhook_endpoint_parses_note_created_event():
|
||||
|
||||
1. **Mock webhook delivery**: POST webhook payloads directly to the `/webhooks/nextcloud` endpoint
|
||||
2. **Verify processing**: Check that documents are queued and eventually appear in Qdrant
|
||||
3. **Test authentication**: Verify requests without valid auth header are rejected (when `WEBHOOK_SECRET` is set)
|
||||
3. **Test authentication**: Verify requests without a valid auth header are rejected (401), and that with no `WEBHOOK_SECRET` the route is absent / the handler returns 503
|
||||
|
||||
```python
|
||||
async def test_webhook_integration_mocked_delivery():
|
||||
|
||||
@@ -552,6 +552,10 @@ async def get_server_status(request: Request) -> JSONResponse:
|
||||
"version": __version__,
|
||||
"auth_mode": "oauth" if settings.enable_oauth else "basic",
|
||||
"vector_sync_enabled": settings.vector_sync_enabled,
|
||||
# Whether the /webhooks/nextcloud receiver is active (gated on
|
||||
# WEBHOOK_SECRET — GHSA-8vh3-g2qg-2h2c). Lets the UI show webhook sync
|
||||
# as available/unavailable.
|
||||
"webhooks_enabled": bool(settings.webhook_secret),
|
||||
"uptime_seconds": get_uptime(),
|
||||
"management_api_version": "v1",
|
||||
})
|
||||
|
||||
@@ -246,14 +246,18 @@ php occ webhook_listeners:add --event "OCA\Tables\Event\RowDeletedEvent" --uri "
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Webhook Authentication
|
||||
Configure `WEBHOOK_SECRET` to require authentication for incoming webhooks:
|
||||
### Webhook Authentication (required — GHSA-8vh3-g2qg-2h2c)
|
||||
`WEBHOOK_SECRET` is **required** to use webhooks. The receiver trusts the
|
||||
`user.uid` in the payload and feeds it to Qdrant, so an unauthenticated POST
|
||||
could delete or re-index any user's embeddings. When `WEBHOOK_SECRET` is unset,
|
||||
the `/webhooks/nextcloud` route is **not mounted** (404) and registration
|
||||
refuses to create webhooks; vector sync still runs via the polling scanner.
|
||||
|
||||
```bash
|
||||
# MCP Server
|
||||
# MCP Server (generate with: python -c "import secrets; print(secrets.token_urlsafe(32))")
|
||||
WEBHOOK_SECRET=<generate-random-secret>
|
||||
|
||||
# Nextcloud webhook registration
|
||||
# Nextcloud webhook registration — the Authorization header is mandatory
|
||||
php occ webhook_listeners:add \
|
||||
--event "..." \
|
||||
--uri "$MCP_URL/webhooks/nextcloud" \
|
||||
|
||||
Reference in New Issue
Block a user