Skip to content

Security report: playlist deletion affected other accounts sharing an ID #9

Description

@D3SOX

An authenticated legacy-sync account could delete another account's playlist by creating and deleting its own playlist with the same ID.

Severity: high. This is a cross-account authorization failure affecting legacy plaintext playlists. Fully migrated encrypted collections are stored separately and are not affected by this query.

Root cause and impact

The playlist primary key is (id, account_id), and the creation endpoint accepts a client-selected playlist ID. The deletion handler checked that the caller owned a playlist with that ID, but the final database deletion filtered only on id.

The result was deletion of every account's matching playlist and cascading deletion of those playlists' video memberships. The attacker does not need the victim's token or password. Predictable IDs such as OpenTubeX's favorites make the attack possible without discovering a random victim-specific identifier.

Vulnerable query:

pub async fn delete_playlist_by_id(
conn: &mut DbConnection,
playlist_id_: &str,
account_id_: &str,
) -> Result<(), DbError> {
// delete linked videos first to ensure database integrity
// TODO: use ON DELETE CASCADE
diesel::delete(
playlist_video_member.filter(
playlist_id
.eq(playlist_id_.to_string())
.and(playlist_video_member_account_id.eq(account_id_)),
),
)
.execute(conn)
.await?;
diesel::delete(playlist.filter(id.eq(playlist_id_.to_string())))
.execute(conn)
.await?;

Composite key: https://github.com/OpenTubeX/sync-server/blob/cfcea1cdcf268f7a3c819e30f085ba2b960f77a1/migrations/sqlite/2026-05-16-052736-0000_playlists/up.sql

Reproduction with disposable accounts

  1. Register two legacy test accounts, A and B.
  2. With B's token, POST /v1/playlists/ with {"id":"favorites","title":"Favorites","description":""} and add a test video.
  3. With A's token, create a playlist using the same ID.
  4. With A's token, DELETE /v1/playlists/favorites.
  5. Before the fix, B's subsequent GET /v1/playlists/favorites returns 404, and B's playlist memberships are gone.

The audit reproduced the failure against an isolated SQLite database using the real migrations and database functions. No production account was targeted.

Fix and compatibility

Fixed by #8, merge commit b0c503efe7f71a16a9d88f4c1ba52027786e71c8.

The final deletion now filters by both playlist ID and authenticated account ID. Existing foreign-key cascades remove only that account's memberships. The API, successful responses, and client-supplied IDs remain unchanged. No database migration or client update is required for this server-side protection.

Validation and deployment

The regression test verifies that deleting one account's playlist removes its memberships while preserving the other account's playlist and video. The full offline SQLite suite passed 52 tests, PostgreSQL compilation passed, and both database variants built for AMD64 and ARM64.

Deployed to the operator's production instance on 2026-09-06 at 06:52 UTC, after the merged-commit build passed. The deployed SQLite image is ghcr.io/opentubex/sync-server@sha256:af8cb65e28dfccc021b656ed5a067ba87d1bd957a20939fe401748469ad6b7df.

Before deployment, HTTP checks against that image with a disposable database passed for playlist isolation, unauthorized group mutations, legitimate owner operations, legacy registration/subscription payloads, and encrypted collection round trips. A consistent production SQLite backup passed PRAGMA quick_check. After deployment, the production health endpoint returned HTTP 200 with the existing capabilities, and the running image matched the tested image.

This is a report of a confirmed code defect. The audit did not establish that production accounts had been exploited. Deployed fixes prevent future unauthorized deletions; they do not reconstruct data that may already have been deleted.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions