Add authentication to 200+ API endpoints (CVE-2026-25505)

Security fix for critical vulnerability (CVSS 9.8) where API endpoints
were accessible without authentication when auth was enabled.

Changes:
- Add RequirePermissionIfAuthEnabled() to all unprotected route files:
  archives, projects, settings, api_keys, groups, cloud, github_backup,
  support, notifications, notification_templates, maintenance, filaments,
  external_links, smart_plugs, discovery, firmware, kprofiles, camera,
  ams_history, pending_uploads, updates, spoolman, system, print_queue,
  printers
- Keep image-serving endpoints (thumbnails, timelapse, photos, camera
  streams, icons) unauthenticated since <img> tags cannot send headers
- Add backend integration tests for endpoint auth enforcement
- Add frontend tests for ownership-based permissions (canModify)

Fixes: CVE-2026-25505
This commit is contained in:
maziggy
2026-02-03 08:44:07 +01:00
parent 5b76cf3623
commit 3fa9ed2b91
28 changed files with 948 additions and 119 deletions
+28 -5
View File
@@ -6,8 +6,11 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import delete, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.auth import RequirePermissionIfAuthEnabled
from backend.app.core.database import get_db
from backend.app.core.permissions import Permission
from backend.app.models.github_backup import GitHubBackupConfig, GitHubBackupLog
from backend.app.models.user import User
from backend.app.schemas.github_backup import (
GitHubBackupConfigCreate,
GitHubBackupConfigResponse,
@@ -48,7 +51,10 @@ def _config_to_response(config: GitHubBackupConfig) -> dict:
@router.get("/config", response_model=GitHubBackupConfigResponse | None)
async def get_config(db: AsyncSession = Depends(get_db)):
async def get_config(
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.GITHUB_BACKUP),
):
"""Get the current GitHub backup configuration."""
result = await db.execute(select(GitHubBackupConfig).limit(1))
config = result.scalar_one_or_none()
@@ -63,6 +69,7 @@ async def get_config(db: AsyncSession = Depends(get_db)):
async def save_config(
config_data: GitHubBackupConfigCreate,
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.GITHUB_BACKUP),
):
"""Create or update GitHub backup configuration.
@@ -121,6 +128,7 @@ async def save_config(
async def update_config(
update_data: GitHubBackupConfigUpdate,
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.GITHUB_BACKUP),
):
"""Partially update GitHub backup configuration."""
result = await db.execute(select(GitHubBackupConfig).limit(1))
@@ -153,7 +161,10 @@ async def update_config(
@router.delete("/config")
async def delete_config(db: AsyncSession = Depends(get_db)):
async def delete_config(
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.GITHUB_BACKUP),
):
"""Delete the GitHub backup configuration and all logs."""
result = await db.execute(select(GitHubBackupConfig).limit(1))
config = result.scalar_one_or_none()
@@ -173,6 +184,7 @@ async def delete_config(db: AsyncSession = Depends(get_db)):
async def test_connection(
repo_url: str = Query(..., description="GitHub repository URL"),
token: str = Query(..., description="Personal Access Token"),
_: User | None = RequirePermissionIfAuthEnabled(Permission.GITHUB_BACKUP),
):
"""Test GitHub connection with provided credentials."""
result = await github_backup_service.test_connection(repo_url, token)
@@ -180,7 +192,10 @@ async def test_connection(
@router.post("/test-stored", response_model=GitHubTestConnectionResponse)
async def test_stored_connection(db: AsyncSession = Depends(get_db)):
async def test_stored_connection(
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.GITHUB_BACKUP),
):
"""Test GitHub connection using stored configuration."""
result = await db.execute(select(GitHubBackupConfig).limit(1))
config = result.scalar_one_or_none()
@@ -196,7 +211,10 @@ async def test_stored_connection(db: AsyncSession = Depends(get_db)):
@router.post("/run", response_model=GitHubBackupTriggerResponse)
async def trigger_backup(db: AsyncSession = Depends(get_db)):
async def trigger_backup(
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.GITHUB_BACKUP),
):
"""Manually trigger a backup."""
result = await db.execute(select(GitHubBackupConfig).limit(1))
config = result.scalar_one_or_none()
@@ -213,7 +231,10 @@ async def trigger_backup(db: AsyncSession = Depends(get_db)):
@router.get("/status", response_model=GitHubBackupStatus)
async def get_status(db: AsyncSession = Depends(get_db)):
async def get_status(
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.GITHUB_BACKUP),
):
"""Get current backup status."""
result = await db.execute(select(GitHubBackupConfig).limit(1))
config = result.scalar_one_or_none()
@@ -245,6 +266,7 @@ async def get_logs(
limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0),
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.GITHUB_BACKUP),
):
"""Get backup logs."""
result = await db.execute(select(GitHubBackupConfig).limit(1))
@@ -282,6 +304,7 @@ async def get_logs(
async def clear_logs(
keep_last: int = Query(default=10, ge=0, le=100, description="Number of recent logs to keep"),
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.GITHUB_BACKUP),
):
"""Clear backup logs, optionally keeping the most recent entries."""
result = await db.execute(select(GitHubBackupConfig).limit(1))