Skip to content

fix(server): require auth on the Tautulli webhook, and stop targetless payloads triggering a full sweep - #366

Open
Davixk wants to merge 1 commit into
sirrobot01:betafrom
Davixk:fix/unauthenticated-tautulli-webhook
Open

Davixk wants to merge 1 commit into
sirrobot01:betafrom
Davixk:fix/unauthenticated-tautulli-webhook

Conversation

@Davixk

@Davixk Davixk commented Jul 29, 2026

Copy link
Copy Markdown

What breaks: POST /webhooks/tautulli requires no credentials, and an untargeted payload sent to it starts a full repair sweep. With destructive repair actions enabled, an unauthenticated request can mass-delete.

Why: the route is registered on the parent router in pkg/server/server.go, outside the authenticated group — every other mutating endpoint sits inside it. Separately, handleTautulli treats "no media id" as "sweep everything": it falls through to svc.RunNow(manager.RepairRunOptions{}), which uses the operator's configured repair settings.

Either alone is awkward; together they mean an anonymous curl with a two-field JSON body can delete library content.

The fix: two changes, both small.

  1. Mount the webhooks behind a router that requires the existing API token. No new auth scheme is introduced. Alongside the standard Authorization: Bearer|Token <token> header, an X-API-Token header and a ?token= / ?apikey= query parameter are accepted — notification agents differ in what they can attach to an outgoing request, and requiring a form some senders cannot produce would just break existing setups. use_auth=false is deliberately unchanged: the whole server is open in that mode and this endpoint is not special-cased around it.
  2. Reject a payload with no media id as 400 instead of sweeping. Nothing about a Tautulli notification implies "sweep the entire library". The check now runs before the repair service is looked up, so there is no remaining path from an untargeted payload to a sweep.

Evidence: running in production on a ~47,000-entry library. Tests cover unauthenticated and wrong-credential rejection, each accepted credential form, the targetless rejection, and the use_auth=false passthrough.

The tests construct the server with a nil manager on purpose: both behaviours must be decided before the repair service is ever reached, so reaching it is itself the failure. Run against the previous behaviour, the auth test doesn't merely fail — it panics inside s.manager.Repair(), which is the defect rendered as a stack trace: an unauthenticated request arriving at the repair service.

… payloads sweeping

POST /webhooks/tautulli was registered on the parent router, outside the
authenticated group, so it required no credentials at all. A payload with no
media id then fell through to svc.RunNow(...) -- a full repair sweep using the
operator's configured settings. With destructive repair actions enabled, an
unauthenticated request could therefore mass-delete.

Two changes:

- Mount the webhooks under a router that requires the existing API token. No new
  auth scheme: the standard Authorization: Bearer|Token header is accepted, and
  so are an X-API-Token header and a ?token= / ?apikey= query parameter, because
  notification agents vary in what they can attach to an outgoing request and a
  fix nobody can adopt is not a fix. use_auth=false is unchanged -- the whole
  server is intentionally open in that mode.

- Reject a payload carrying no media id with 400 instead of sweeping. Nothing
  about a Tautulli notification implies "sweep the entire library". The check
  now runs before the repair service is looked up, so no path remains from an
  untargeted payload to a sweep.

Tests cover both, plus the accepted credential forms and the use_auth=false
passthrough. They construct the server with a nil manager deliberately: both
behaviours must be decided before the repair service is reached, so reaching it
is itself the failure. Against the previous behaviour the auth test does not
merely fail -- it panics inside s.manager.Repair(), which is the defect stated
as a stack trace.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant