Skip to content

[BUG] Saved Views API Exposes All Users' Data — Missing Authentication and Owner Isolation #1743

Description

@ionfwsrijan

Description

The Saved Views API endpoints at /api/v1/saved-views have no authentication requirements and no owner-scoped data isolation. Any unauthenticated user (or any authenticated user) can list, create, update, and delete saved views belonging to any other user.

Specific vulnerabilities:

  1. GET /api/v1/saved-views (line 98-106): Returns ALL saved views for ALL users without any authentication or filtering. An attacker can enumerate all saved filter configurations, which may contain sensitive information about scanning targets, search queries, and date ranges.

  2. POST /api/v1/saved-views (line 109-136): Creates saved views with no owner_id attribution. There is no column for owner_id in the saved_views table (see database.py — no owner_id field in the saved views schema at lines 300+).

  3. PUT /api/v1/saved-views/{id} (line 139-182): Updates any saved view by ID. An attacker who can guess or enumerate view IDs can modify others' saved views.

  4. DELETE /api/v1/saved-views/{id} (line 185-190): Deletes any saved view by ID. An attacker can delete all saved views in the system.

The router at saved_views.py:12 is mounted WITHOUT the require_api_key dependency that all other routes use:

saved_views_router = APIRouter(prefix="/api/v1/saved-views", tags=["saved-views"])

Compare with routes.py:133:

router = APIRouter(prefix="/api/v1", dependencies=[Depends(require_api_key)])

Steps to Reproduce

  1. Without any API key or authentication, send:
    GET /api/v1/saved-views
    
  2. Observe the response contains all saved views from all users, including:
    {
      "views": [
        {
          "id": "uuid-1",
          "name": "Critical vulns on prod",
          "filter_json": "{\"severity\":\"critical\",\"target\":\"internal-prod.example.com\",\"searchQuery\":\"admin\"}",
          "created_at": "...",
          "updated_at": "..."
        },
        {
          "id": "uuid-2",
          "name": "Internal network scan targets",
          "filter_json": "{\"severity\":\"all\",\"target\":\"10.0.0.0/8\"}",
          ...
        }
      ],
      "total": 47
    }
  3. Delete all saved views:
    DELETE /api/v1/saved-views/uuid-1
    DELETE /api/v1/saved-views/uuid-2
    ...
    

Expected Behavior

Saved views should be:

  • Authenticated (require a valid API key or session)
  • Scoped to the authenticated user (only return views belonging to the requesting user)
  • Protected against unauthorized modification or deletion

Actual Behavior

All saved views endpoints are completely unauthenticated and unscoped.

Root Cause

Two issues compound:

  1. Missing authentication: The router at saved_views.py:12 does not include the require_api_key dependency, unlike all other API routes in routes.py:133.

  2. Missing owner isolation: The saved_views database schema in database.py does not have an owner_id column. Even if authentication were added, there would be no way to scope views to a specific user.

Current schema (from database.py):

CREATE TABLE IF NOT EXISTS saved_views (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    filter_json TEXT NOT NULL DEFAULT '{}',
    created_at TIMESTAMP NOT NULL DEFAULT (datetime('now')),
    updated_at TIMESTAMP
);

Proposed Fix

Step 1: Add owner_id Column (Database Migration)

Create a new migration file:

-- migrations/00X_add_saved_views_owner_id.sql
ALTER TABLE saved_views ADD COLUMN owner_id TEXT NOT NULL DEFAULT 'default';

CREATE INDEX IF NOT EXISTS idx_saved_views_owner ON saved_views(owner_id);

-- Migrate existing rows to 'default' owner (or delete them if unsafe)
UPDATE saved_views SET owner_id = 'default' WHERE owner_id IS NULL;

Step 2: Add Authentication to the Router

from .auth import require_api_key, get_current_owner

saved_views_router = APIRouter(
    prefix="/api/v1/saved-views",
    tags=["saved-views"],
    dependencies=[Depends(require_api_key)],  # Add auth dependency
)

Step 3: Scope All Queries to Authenticated User

@saved_views_router.get("")
async def list_saved_views(request: Request) -> Dict[str, Any]:
    """Return saved views for the authenticated user."""
    owner_id = get_current_owner(request)  # Extract from API key / session
    db = await get_db()
    rows = await db.fetchall(
        "SELECT id, name, filter_json, created_at, updated_at "
        "FROM saved_views WHERE owner_id = ? ORDER BY created_at ASC",
        (owner_id,),
    )
    return {"views": rows, "total": len(rows)}

@saved_views_router.post("", status_code=201)
async def create_saved_view(body: SavedViewCreate, request: Request) -> Dict[str, Any]:
    owner_id = get_current_owner(request)
    db = await get_db()
    
    # Check for name collision within this owner's views
    existing = await db.fetchone(
        "SELECT id FROM saved_views WHERE owner_id = ? AND LOWER(name) = LOWER(?)",
        (owner_id, body.name),
    )
    if existing:
        raise HTTPException(status_code=409, detail=f"A saved view named '{body.name}' already exists.")
    
    view_id = str(uuid.uuid4())
    await db.execute(
        "INSERT INTO saved_views (id, owner_id, name, filter_json) VALUES (?, ?, ?, ?)",
        (view_id, owner_id, body.name, body.filter_json),
    )
    return {"id": view_id, "name": body.name, "created": True}

Similarly scope PUT and DELETE to the authenticated owner's views.

Step 4: Backfill Migration for Existing Deployments

For existing deployments, if the database currently has views from multiple users that cannot be attributed, either:

  • Assign all existing views to a single admin account
  • Or delete unowned views (safest — the data is filter presets, not critical data)

Activity

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

Metadata

Metadata

Assignees

Labels

area:backendBackend API, database, or service workarea:securitySecurity-sensitive implementation or testslevel:advanced55 pts difficulty label for advanced contributor PRspriority:mediumImportant issue with normal urgencytype:bugBug fix work category bonus labeltype:securitySecurity work category bonus label

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions