Save DReazer/1e476832e48bcdb2b4f732689dfeac0c to your computer and use it in GitHub Desktop.
Vendor: MODSetter Product: SurfSense ( ) Affected version: 0.0.36 (verified on official image ghcr.io/modsetter/surfsense-backend:0.0.36 ) Component: FastAPI backend, Circleback meeting webhook Vulnerability class: Missing Authentication / Broken Access Control CWE: CWE-306 (Missing Authentication for Critical Function), CWE-862 (Missing Authorization) Attack type: Remote Authentication required: No User interaction: None
SurfSense 0.0.36 exposes POST /api/v1/webhooks/circleback/{workspace_id} with no authentication and no signature check. The handler docstring states that the route is intentionally unauthenticated and that signature verification “can be added later.”
workspace_id is a public sequential integer. An unauthenticated caller can queue a meeting payload for any workspace. Celery then creates a CIRCLEBACK document, converts the body to Markdown, embeds it, and marks it ready . A Circleback connector is not required; the document is still stored and attributed to the workspace owner.
The same path can overwrite an existing meeting when circleback_{meeting_id} already exists. Injected text is indexed into the workspace knowledge base and becomes searchable / usable by chat.
GET /api/v1/webhooks/circleback/{workspace_id}/info is also unauthenticated and returns the webhook URL for that id.
CVSS 3.1 (recommended): 7.5 High CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
AV:N — HTTP webhook, no credentials
AC:L — one JSON POST; workspace ids are small sequential integers
PR:N — no token, cookie, or HMAC
UI:N — no victim action
S:U — impact is the application data store
C:N — this route writes; it does not return other tenants’ existing documents
I:H — create / overwrite documents in any workspace
A:N — availability impact was not demonstrated
On a shared multi-tenant SaaS the same bug is still S:U : other tenants are the same vulnerable application, already covered by I:H. Do not raise Confidentiality unless a later RAG/chat leak of pre-existing victim documents is shown on this same finding.
CVSS 4.0 (recommended for VulDB): 8.7 High CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N NVD calculator:
Do not score VC:H / C:H unless a later RAG/chat leak of other users’ existing data is shown on this same finding.
surfsense_backend/app/routes/circleback_webhook_route.py — receive_circleback_webhook() (lines 215–305):
no Depends(get_auth_context)
no HMAC / shared-secret header
missing Circleback connector is a warning only; the write still proceeds
workspace_id is taken from the URL path
surfsense_backend/app/routes/circleback_webhook_route.py — get_circleback_webhook_info() (lines 315–344) is also unauthenticated and returns the webhook URL for any integer id.
surfsense_backend/app/tasks/document_processors/circleback_processor.py — add_circleback_meeting_document() writes a Document with document_type=CIRCLEBACK . If no connector exists it falls back to the workspace owner as created_by . Same circleback_{meeting_id} updates the existing row when content changes.
Router is mounted at /api/v1 in surfsense_backend/app/routes/__init__.py .
Reproduction (self-hosted Docker 0.0.36)
Tested 2026-08-17 against ghcr.io/modsetter/surfsense-backend:0.0.36 . Public entry via Caddy: .
Two existing workspaces (no Circleback connector on either):
Note — the number in the path is the victim workspace_id . POST /api/v1/webhooks/circleback/1 writes into workspace 1 . POST /api/v1/webhooks/circleback/2 writes into workspace 2 . There is no session. The integer is not “your” workspace and is not a secret; it is the target tenant. Incrementing 1 , 2 , 3 , … enumerates workspaces. GET /api/v1/workspaces (authenticated) returns each user’s own id if you need to map accounts in a lab.
Note — the number in the path is the victim workspace_id . POST /api/v1/webhooks/circleback/1 writes into workspace 1 . POST /api/v1/webhooks/circleback/2 writes into workspace 2 . There is no session. The integer is not “your” workspace and is not a secret; it is the target tenant. Incrementing 1 , 2 , 3 , … enumerates workspaces. GET /api/v1/workspaces (authenticated) returns each user’s own id if you need to map accounts in a lab.
1. Unauthenticated info leak
HTTP 200, no cookie / Authorization . Body includes webhook_url and workspace_id .
2. Unauthenticated write (no Authorization header)
Repeat against /webhooks/circleback/2 with a different id / name .
Both return HTTP 200 :
3. Confirm documents in both tenants
After the Celery worker finishes (a few seconds), either query the database or log into the victim account and open that workspace’s Documents list. The injected meeting appears as a CIRCLEBACK document.
Alternatively, instead of querying SQL statements, logging into the account and viewing the relevant knowledge base reveals that the documentation has been written to without authorization.
Markdown includes the attacker notes field. No login was used for the POST.
Figure: Unauthenticated POST /api/v1/webhooks/circleback/1 (no Authorization header) returns HTTP 200 accepted . The number 1 in the path is the target workspace id. After processing, the injected document ( cb-unauth-atk-ws1 ) appears in that workspace’s Documents list.
Expected: reject unsigned / unauthenticated webhook calls; require a per-workspace secret; 404 if no Circleback connector exists. Actual: any client that can guess an integer workspace id writes into that knowledge base.
An unauthenticated attacker on the network can:
Inject arbitrary meeting documents into every enumerable workspace.
Overwrite a injection (or a real Circleback meeting) by reusing id .
Poison RAG / chat answers for members of that workspace.
Consume embedding / ETL capacity.
No account, invite, or connector setup is required.
Require a per-workspace HMAC or shared secret on every Circleback POST; reject missing or invalid signatures.
Return 404 when the workspace has no Circleback connector.
Do not take workspace_id from the URL alone; bind it to the verified secret.
Disable or unmount the route until signing is implemented.
Authenticate or remove GET .../info .
Repository:
Release:
Image: ghcr.io/modsetter/surfsense-backend:0.0.36
Routes: POST /api/v1/webhooks/circleback/{workspace_id} , GET /api/v1/webhooks/circleback/{workspace_id}/info
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
