Files

9.9 KiB
Raw Permalink Blame History

phase, slug, status, threats_open, asvs_level, created
phase slug status threats_open asvs_level created
03 document-migration-multi-user-isolation verified 0 2 2026-06-01

Phase 03 — Security

Per-phase security contract: threat register, accepted risks, and audit trail.


Trust Boundaries

Boundary Description Data Crossing
migration runtime → MinIO DDL transaction holds DB rows; MinIO deletes happen outside any DB transaction Object keys (UUIDs)
test fixtures → backend code Fixtures fabricate JWTs that hit every guarded endpoint — must not leak across tests Short-lived JWT tokens
browser → MinIO (presigned PUT) Time-limited presigned URL; MinIO authenticates via HMAC; no Authorization header from browser File bytes
browser → FastAPI /confirm Authenticated user provides only document_id; FastAPI reads size_bytes from MinIO stat Document ID
FastAPI /confirm → quotas table Concurrent /confirm calls race against Quota row; atomic UPDATE WHERE prevents overflow Byte counts
Celery beat → DB+MinIO Runs as docuvault_app role; deletes only its own pending rows and MinIO objects Document metadata, object keys
browser → /api/documents/* Bearer JWT; ownership assertion gates every resource read/write Document content, metadata
browser → /api/topics/* Bearer JWT; namespace filter prevents cross-user topic enumeration Topic labels
admin token → /api/documents/* Admin role explicitly rejected 403 — cannot access document content (blocked)
Celery task → users table AI config resolved via doc.user_id → users row; no user-controlled provider input AI provider/model config
admin → /api/admin/users/{id}/ai-config Only admin write path to user.ai_provider / user.ai_model AI provider selection

Threat Register

Threat ID Category Component Disposition Mitigation Status
T-03-01 Tampering migrations/versions/0003_multi_user_isolation.py mitigate null_user_objects collected via SELECT before DELETE; each remove_object() wrapped in try/except Exception: pass (lines 5688) closed
T-03-02 Denial of Service Alembic migration when MinIO unreachable accept See Accepted Risks Log closed
T-03-03 Information Disclosure tests/conftest.py — xfail fixture JWTs mitigate create_access_token uses standard auth service with test secret; async_client clears dependency_overrides on teardown (line 155); no token values logged closed
T-03-04 Spoofing api/documents.py — upload-url endpoint mitigate object_key = f"{current_user.id}/{doc_id}/{uuid.uuid4()}{suffix}" computed server-side (lines 112113); filename stored in DB only; extension from Path(body.filename).suffix.lower() closed
T-03-05 Tampering api/documents.py — confirm endpoint mitigate size = await get_storage_backend().stat_object(doc.object_key) from MinIO authoritative source (line 327); no size param in confirm body closed
T-03-06 Denial of Service api/documents.py — concurrent /confirm at quota boundary mitigate Atomic SQL: UPDATE quotas SET used_bytes = used_bytes + :delta WHERE … AND (used_bytes + :delta) <= limit_bytes RETURNING; fetchone() None → HTTP 413 (lines 341351) closed
T-03-07 Information Disclosure Presigned URL leakage in logs accept See Accepted Risks Log closed
T-03-08 Repudiation tasks/document_tasks.py — abandoned upload orphans mitigate cleanup_abandoned_uploads Celery beat task present (lines 132177); celery_app.py beat_schedule runs every 30 min (lines 4346) closed
T-03-09 Information Disclosure docker-compose.yml — MinIO CORS mitigate MINIO_API_CORS_ALLOW_ORIGIN: ${FRONTEND_URL:-http://localhost:5173} — explicit non-wildcard origin (line 26) closed
T-03-10 Tampering storage/minio_backend.py — Docker hostname in presigned URL mitigate Dual MinIO client: self._client for internal ops (stat/get/delete), self._public_client for generate_presigned_put_url (lines 5460, 154, 169) closed
T-03-11 Information Disclosure api/documents.py — cross-user doc access mitigate if doc is None or doc.user_id != current_user.id: raise HTTPException(404) at lines 322323, 545546, 579580, 633634, 702703, 767 — returns 404 not 403 closed
T-03-12 Elevation of Privilege api/documents.py — admin reading doc content mitigate get_regular_user raises 403 for admin role (deps/auth.py:95109); Depends(get_regular_user) on all document handlers at lines 99, 143, 302, 416, 530, 557, 613, 688, 742 closed
T-03-13 Information Disclosure api/topics.py — cross-user topic enumeration mitigate All queries filter: or_(Topic.user_id == current_user.id, Topic.user_id.is_(None)); create_topic scoped by user_id=current_user.id closed
T-03-14 Elevation of Privilege api/topics.py, api/admin.py — regular user creating system topic mitigate POST /api/admin/topics uses Depends(get_current_admin) and creates user_id=None; regular POST /api/topics forces user_id=current_user.id closed
T-03-15 Tampering api/documents.py — object_key forged with another user's UUID prefix mitigate object_key = f"{current_user.id}/{doc_id}/{uuid.uuid4()}{suffix}" — prefix always from current_user.id; no user-supplied prefix accepted closed
T-03-16 Spoofing api/documents.py — anonymous traffic mitigate HTTPBearer() with auto_error=True raises 403 on missing header (deps/auth.py:35); get_current_user raises 401 on invalid/expired token (lines 5255) closed
T-03-17 Elevation of Privilege /api/settings endpoint mitigate backend/api/settings.py absent; main.py contains no settings_router reference — endpoint fully removed closed
T-03-18 Information Disclosure services/storage.py — settings.json flat file mitigate No load_settings, save_settings function bodies present; settings.json no longer read or written; API keys in env only closed
T-03-19 Tampering tasks/document_tasks.py — Celery task ai_provider injection mitigate Task signature is document_id: str only; user.ai_provider resolved inside _run() from DB lookup (lines 6264) closed
T-03-20 Information Disclosure system_prompt env var in container logs accept See Accepted Risks Log closed
T-03-21 Repudiation frontend/src/views/SettingsView.vue — old API calls mitigate getSettings/patchSettings/testProvider/getDefaultPrompt absent from api/client.js; SettingsView.vue is static display only closed
T-03-22 Information Disclosure stores/documents.js — XHR PUT Authorization header mitigate Only Content-Type header set via setRequestHeader; no Authorization header in uploadToMinIO helper (line 2425, comment cites T-03-22) closed
T-03-23 Spoofing components/upload/UploadProgress.vue — client-side quota from file.size mitigate All three values (rejected_bytes, used_bytes, limit_bytes) read from item.quotaError populated by 413 response body; no file.size used (lines 27, 30) closed
T-03-24 Denial of Service Concurrent uploads exhaust browser memory accept See Accepted Risks Log closed
T-03-25 Tampering stores/documents.js — upload state race condition mitigate const rowKey = \${file.name}__${Date.now()}`` composite key prevents collisions on duplicate filename uploads (line 70) closed
T-03-26 Repudiation stores/auth.js — quota refetch silent failure mitigate fetchQuota() wrapped in try/catch; QuotaBar.vue uses v-if="!loadFailed" to hide on error (auth.js:144149) closed
T-03-SC Tampering Package managers (pip/npm) — all 5 plans mitigate No new pip or npm dependencies added across all 5 plans; all packages were already pinned in Phase 1/2 requirements.txt and package-lock.json closed

Status: open · closed Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)


Accepted Risks Log

Risk ID Threat Ref Rationale Accepted By Date
AR-03-01 T-03-02 Alembic migration runs only after docker-compose health checks confirm MinIO availability. If MinIO is unreachable during migration, the migration exits without data loss (DB-side changes roll back; MinIO deletes are skipped). The deploy can be retried on next startup. Documented in upgrade() docstring. orchestrator 2026-06-01
AR-03-02 T-03-07 Presigned PUT URLs have a 15-minute TTL and are single-use per object key. If leaked, the worst-case outcome is an attacker completing an already-authorized upload within the window. Full URL is never logged — only document_id is logged. Risk is low for v1. orchestrator 2026-06-01
AR-03-03 T-03-20 SYSTEM_PROMPT is a static AI instruction string containing no PII, credentials, or user data. It is safe to appear in container logs. Not a sensitive env var. orchestrator 2026-06-01
AR-03-04 T-03-24 XHR-based uploads stream bytes natively without buffering to JavaScript memory. Browser memory exhaustion from concurrent uploads is a user-driven concurrency issue acceptable for v1 scope. No concurrent upload limit enforced in the frontend. orchestrator 2026-06-01

Security Audit Trail

Audit Date Threats Total Closed Open Run By
2026-06-01 27 (26 functional + T-03-SC) 27 0 gsd-security-auditor (sonnet)

Sign-Off

  • All threats have a disposition (mitigate / accept / transfer)
  • Accepted risks documented in Accepted Risks Log
  • threats_open: 0 confirmed
  • status: verified set in frontmatter

Approval: verified 2026-06-01