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
@@ -24,6 +24,7 @@ from nextcloud_mcp_server.api.webhooks import (
|
||||
list_webhooks,
|
||||
)
|
||||
from nextcloud_mcp_server.auth.scope_authorization import ProvisioningRequiredError
|
||||
from nextcloud_mcp_server.auth.webhook_routes import WebhookSecretNotConfigured
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
@@ -145,7 +146,7 @@ async def test_create_webhook_uses_basic_auth(mocker):
|
||||
)
|
||||
mocker.patch(
|
||||
"nextcloud_mcp_server.api.webhooks.webhook_auth_pair",
|
||||
return_value=("none", None),
|
||||
return_value=("header", {"Authorization": "Bearer supersecret"}),
|
||||
)
|
||||
|
||||
client = TestClient(_build_test_app())
|
||||
@@ -163,6 +164,31 @@ async def test_create_webhook_uses_basic_auth(mocker):
|
||||
_assert_basic_auth_not_bearer(factory)
|
||||
|
||||
|
||||
async def test_create_webhook_returns_503_when_secret_unset(mocker):
|
||||
"""Security (GHSA-8vh3-g2qg-2h2c): registration is refused without a
|
||||
WEBHOOK_SECRET so no unauthenticated delivery target is created."""
|
||||
_patch_token_validation(mocker)
|
||||
_patch_basic_auth(mocker, username="bob", app_password="bob-pwd")
|
||||
_patch_outbound_client_factory(mocker)
|
||||
mocker.patch(
|
||||
"nextcloud_mcp_server.api.webhooks.webhook_auth_pair",
|
||||
side_effect=WebhookSecretNotConfigured("WEBHOOK_SECRET must be set"),
|
||||
)
|
||||
|
||||
client = TestClient(_build_test_app())
|
||||
resp = client.post(
|
||||
"/api/v1/webhooks",
|
||||
headers={"Authorization": "Bearer mcp-token"},
|
||||
json={
|
||||
"event": "OCP\\Events\\NodeCreated",
|
||||
"uri": "http://mcp:8000/webhooks/nextcloud",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 503
|
||||
assert resp.json()["error"] == "Webhooks disabled"
|
||||
|
||||
|
||||
async def test_create_webhook_validates_required_fields(mocker):
|
||||
_patch_token_validation(mocker)
|
||||
_patch_basic_auth(mocker)
|
||||
|
||||
Reference in New Issue
Block a user