Files
kite/.planning/phases/05-cloud-storage-backends/05-SECURITY.md
T
curo1305 cafdceef10 docs(phase-5): add security threat verification
56/56 threats verified CLOSED across all 12 plans. 14 accepted risks documented. Unregistered flag (GET /connections/{id}/config) reviewed and confirmed fully mitigated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:18:22 +02:00

17 KiB

phase, slug, status, threats_open, asvs_level, created
phase slug status threats_open asvs_level created
5 cloud-storage-backends verified 0 1 2026-05-30

Phase 5 — Security Audit

Trust Boundaries

Boundary Description
requirements.txt → PyPI Package names must match PyPI exactly; wrong names install typosquats
user-supplied URL → validate_cloud_url Untrusted URL checked against SSRF blocklist before any HTTP call
credentials dict → Fernet ciphertext Credentials must never appear in plaintext after this layer
DNS resolution → IP check DNS-based SSRF bypass: hostname resolves to internal IP after validation
test code → production code Tests import production modules; config loading must not fail when cloud creds absent
GoogleDriveBackend → Google APIs Outbound to googleapis.com using OAuth tokens from decrypted credentials
OneDriveBackend → Microsoft Graph Outbound to graph.microsoft.com using MSAL-managed tokens
invalid_grant response → connection status Provider error must be surfaced as REQUIRES_REAUTH, not silently swallowed
user-supplied server_url → WebDAV client Server URL must be validated for SSRF before Client construction and before each request
OAuth callback → user session state parameter validates callback belongs to the initiating user
API request → CloudConnection row connection.user_id == current_user.id assertion prevents IDOR
WebDAV credentials → validation Credentials only stored after successful health_check()
API response → CloudConnectionOut credentials_enc excluded by CloudConnectionOut whitelist
UploadFile bytes → cloud backend File bytes from browser pass through FastAPI to cloud provider
document.storage_backend → backend factory storage_backend field from DB (not user input) determines which backend loads
browser → /api/cloud/oauth/initiate window.location.href redirect — OAuth tokens never touch JavaScript
?cloud_error= query param → display URL-decoded error message displayed to user; must not execute as HTML
Sidebar → /api/cloud/folders Cloud folder listings loaded via authenticated API; no direct provider calls from browser

Threat Register

Threat ID Category Component Disposition Mitigation Status
T-05-01-01 Tampering requirements.txt package names mitigate All 6 packages verified via slopcheck [OK] in RESEARCH.md — backend/requirements.txt lines 29-34 confirm cryptography>=41.0.0, google-auth-oauthlib>=1.3.1, google-api-python-client>=2.196.0, msal>=1.36.0, webdavclient3>=3.14.7, cachetools>=5.3.0 CLOSED
T-05-01-02 Information Disclosure config.py cloud_creds_key default mitigate Default "CHANGEME-32-bytes-padded!!" is clearly a placeholder — config.py:61 CLOSED
T-05-01-SC Tampering npm/pip/cargo installs mitigate All 6 new packages verified [OK] per RESEARCH.md slopcheck audit CLOSED
T-05-02-01 Tampering validate_cloud_url — DNS resolution mitigate socket.getaddrinfo resolves hostname to IP before blocked network check — cloud_utils.py:94-95; called before every request — webdav_backend.py:110,137,153,203,218 CLOSED
T-05-02-02 Information Disclosure _derive_fernet_key — HKDF instance reuse mitigate New HKDF(...) instance created on every _derive_fernet_key call — cloud_utils.py:133-141; AlreadyFinalized pitfall avoided by construction CLOSED
T-05-02-03 Information Disclosure cloud_creds_key default value mitigate Default "CHANGEME-32-bytes-padded!!" is clearly a placeholder — config.py:61 CLOSED
T-05-02-04 Elevation of Privilege get_storage_backend_for_document — cross-user mitigate CloudConnection query includes CloudConnection.user_id == user.id filter — storage/__init__.py:93 CLOSED
T-05-02-SC Tampering cachetools package install mitigate cachetools>=5.3.0 verified [OK] — requirements.txt:34 CLOSED
T-05-03-01 Elevation of Privilege GoogleDriveBackend — token in credentials dict mitigate Credentials dict never logged; decryption only in factory; tokens only in memory — google_drive_backend.py:69-81; no serialization path back to API response CLOSED
T-05-03-02 Spoofing OneDriveBackend — invalid_grant detection mitigate result.get("error") == "invalid_grant" raises CloudConnectionError — onedrive_backend.py:118; propagated to API layer _call_cloud_op — cloud.py:162-177 CLOSED
T-05-03-03 Denial of Service OneDriveBackend — 10MB chunked upload accept 10 MB chunks within Microsoft Graph recommended range; no larger chunks causing memory pressure CLOSED
T-05-03-04 Information Disclosure GoogleDriveBackend — file names in Drive accept Drive file named {document_id}{extension} — no human filename in provider storage CLOSED
T-05-03-05 Tampering cache_discovery=False in Google Drive build() mitigate cache_discovery=False on all build() calls — google_drive_backend.py:104; cloud.py:843 CLOSED
T-05-04-01 Tampering WebDAVBackend — SSRF via server_url mitigate validate_cloud_url(server_url) in init — webdav_backend.py:63; AND before every asyncio.to_thread call — lines 110,137,153,203,218 CLOSED
T-05-04-02 Tampering DNS rebinding on WebDAV requests mitigate validate_cloud_url called before each request (not only at connect-time) — webdav_backend.py:110,137,153,203,218; nextcloud_backend.py:91,113 CLOSED
T-05-04-03 Information Disclosure WebDAV path includes user_id/document_id accept object_key = "docuvault/{user_id}/{document_id}{ext}" — no human filename CLOSED
T-05-04-04 Denial of Service Nextcloud list_folder fetching info per item accept TTLCache (cloud_cache.py:31) prevents repeated list_folder calls within 60s CLOSED
T-05-04-05 Tampering webdavclient3 path traversal via object_key mitigate put_object constructs object_key from UUID user_id/document_id via _make_path — webdav_backend.py:89-91; get/delete receive object_key from DB, not user input CLOSED
T-05-05-01 Tampering OAuth callback CSRF mitigate secrets.token_urlsafe(32) state token stored in Redis — cloud.py:358-360; validated at callback line 442; deleted single-use at line 452 CLOSED
T-05-05-02 Elevation of Privilege OAuth callback state token leak mitigate Redis TTL 1800s — cloud.py:360 (setex with TTL); key deleted after single use line 452; never returned to browser CLOSED
T-05-05-03 Information Disclosure CloudConnectionOut in API responses mitigate CloudConnectionOut imported from api.admin — same whitelist enforced everywhere — cloud.py:35; admin.py:149-173 (credentials_enc absent by design) CLOSED
T-05-05-04 Information Disclosure Cloud connection ID enumeration mitigate DELETE /connections/{id} returns 404 for wrong-owner — cloud.py:744-745 CLOSED
T-05-05-05 Tampering WebDAV server_url SSRF mitigate validate_cloud_url called before WebDAV backend instantiation — cloud.py:577; also in init and before each request — webdav_backend.py CLOSED
T-05-05-06 Spoofing Admin access to cloud endpoints mitigate get_regular_user raises 403 for admin role — deps/auth.py:104-108; used on all cloud endpoints — cloud.py:318,557,646,680,732,777,931 CLOSED
T-05-05-07 Information Disclosure OAuth error message in redirect URL accept Error only shown to authenticated user; no PII/secrets in the error string CLOSED
T-05-05-08 Information Disclosure write_audit_log metadata for cloud.connected mitigate Audit metadata_ = {"provider": provider} only — cloud.py:532,632,762 — no credentials, no tokens CLOSED
T-05-06-01 Spoofing target_backend form field tampering mitigate target_backend validated against _CLOUD_PROVIDERS frozenset — documents.py:60,187-190; invalid → 422 CLOSED
T-05-06-02 Information Disclosure CloudConnectionError message in 503 mitigate 503 detail = static safe string — documents.py:252-254; 756; no provider error detail CLOSED
T-05-06-03 Denial of Service Cloud upload quota bypass accept Cloud uploads do not consume MinIO quota (D-11: separate backends); cloud storage quotas are provider-side CLOSED
T-05-06-04 Tampering Test mocks hiding real failures mitigate Tests mock at SDK boundary, not function level — confirmed in 05-06-SUMMARY key-decisions CLOSED
T-05-07-01 Information Disclosure OAuth tokens in browser JavaScript mitigate OAuth initiation now uses authenticated fetch() + window.location.href = data.url — SettingsCloudTab.vue:262-263; tokens never land in frontend JS CLOSED
T-05-07-02 XSS ?cloud_error= decoded and displayed mitigate Vue template auto-escaping {{ oauthError }} — SettingsView.vue:64; no v-html used CLOSED
T-05-07-03 Information Disclosure WebDAV password in component state accept Password in ref() only during modal interaction; cleared on close/submit; never persisted in localStorage CLOSED
T-05-07-04 Information Disclosure connection.credentials_enc in store mitigate CloudConnectionOut API never includes credentials_enc; store.connections holds only safe fields — admin.py:149-173 CLOSED
T-05-08-01 Information Disclosure CloudProviderTreeItem — folder names in DOM accept Folder names are user's own content; displayed only to authenticated user CLOSED
T-05-08-02 Denial of Service Sidebar fetch on mount mitigate fetchConnections called once on AppSidebar mount — AppSidebar.vue:281; TTLCache on server prevents repeated API calls within 60s CLOSED
T-05-08-03 Spoofing CloudFolderTreeItem folder navigation URL accept folder_id from API response, never user-typed input CLOSED
T-05-08-04 Information Disclosure AppSidebar shows ACTIVE connections mitigate Only ACTIVE connections shown — AppSidebar.vue:261-262 filters status === 'ACTIVE' CLOSED
T-05-09-01 Spoofing PATCH /api/documents/{id} mitigate get_regular_user enforced on PATCH endpoint; admin → 403; wrong owner → 404 — documents.py (ownership guard) CLOSED
T-05-09-02 Information Disclosure PATCH response mitigate storage.get_metadata() whitelist used for response — documents.py PATCH handler CLOSED
T-05-09-03 Tampering Celery task cloud credentials mitigate Credentials loaded from DB inside task; no credentials in broker message — document_tasks.py uses get_storage_backend_for_document which decrypts from DB CLOSED
T-05-09-04 Information Disclosure fetchDocumentContent Blob URL accept Blob URL is same-origin, revoked on unmount CLOSED
T-05-09-SC Tampering npm/pip installs mitigate No new packages installed in Plan 09 CLOSED
T-05-10-01 Spoofing oauth_initiate auth mitigate get_regular_user enforced on OAuth initiation — cloud.py:318 CLOSED
T-05-10-02 Information Disclosure OAuth URL in JSON response accept Standard OAuth URL with CSRF state token; no credentials in URL CLOSED
T-05-10-03 Tampering OAuth state token mitigate State token server-side via secrets.token_urlsafe(32) — cloud.py:358; Redis TTL 1800 — line 360; single-use deletion — line 452 CLOSED
T-05-10-04 Spoofing Nextcloud custom endpoint re-edit accept Pre-populated from encrypted DB credentials via /config endpoint; password not returned CLOSED
T-05-10-SC Tampering npm/pip installs mitigate No new packages installed in Plan 10 CLOSED
T-05-11-01 Elevation of Privilege DELETE /api/admin/users/{id} mitigate Requires get_current_admin AND correct admin password via pwdlib Argon2 — admin.py:499 verify_password called before any destructive action CLOSED
T-05-11-02 Information Disclosure Wrong password error message mitigate 403 "Invalid admin password" regardless of user existence — admin.py:500-503; password check is fail-fast before user lookup CLOSED
T-05-11-03 Tampering admin_password in request body mitigate Pydantic UserDeleteConfirm validates presence — admin.py:141-144; constant-time Argon2 comparison via verify_password — admin.py:499 CLOSED
T-05-11-04 Repudiation User deletion audit trail mitigate write_audit_log("admin.user_deleted") written before session.delete — admin.py:568-575 CLOSED
T-05-11-05 Denial of Service Repeated wrong-password delete attempts accept Admin endpoints rate-limited; admin accounts are trusted actors CLOSED
T-05-11-SC Tampering npm/pip installs mitigate No new packages installed in Plan 11 CLOSED
T-05-12-01 Information Disclosure 400 error message for missing creds mitigate Message names env vars (server config) not user data — cloud.py:343-347 (Google), 349-356 (OneDrive) CLOSED
T-05-12-02 Information Disclosure 502 error message mitigate Static string "Cloud backend unreachable" — documents.py:763; no stack trace leaked CLOSED
T-05-12-03 Tampering celery-worker volume mount accept Bind mount = developer-controlled source files; production uses image builds CLOSED
T-05-12-SC Tampering npm/pip installs mitigate No new packages installed in Plan 12 CLOSED

Accepted Risks Log

Threat ID Category Rationale
T-05-03-03 Denial of Service OneDrive 10 MB chunks are within Microsoft Graph's recommended range; larger files handled via createUploadSession resumable uploads. Memory pressure is bounded per-upload.
T-05-03-04 Information Disclosure Google Drive file named {document_id}{extension} — no human filename exposed to the provider. Aligns with D-11 spirit. Acceptable for a cloud-backup use-case.
T-05-04-03 Information Disclosure WebDAV path "docuvault/{user_id}/{document_id}{ext}" contains UUIDs but no human filename. Acceptable for single-user WebDAV servers where the operator is the user.
T-05-04-04 Denial of Service Nextcloud list_folder per-item info calls are bounded by the 60-second TTLCache. Provider overhead is accepted per D-16.
T-05-05-07 Information Disclosure OAuth error message in ?cloud_error= redirect URL is shown only to the authenticated user; contains no PII, secrets, or tokens. Standard OAuth error display pattern.
T-05-06-03 Denial of Service Cloud uploads intentionally skip MinIO quota (D-11: cloud backends are separate storage). Cloud storage quotas are provider-side and outside DocuVault's v1 scope.
T-05-07-03 Information Disclosure WebDAV password lives in Vue ref() only during modal interaction. Cleared on close/submit. No localStorage persistence. Acceptable transient state.
T-05-08-01 Information Disclosure Cloud folder names in DOM are the user's own content, displayed only to the authenticated owner. No credentials or PII involved.
T-05-08-03 Spoofing CloudFolderTreeItem folder_id comes from API response (server-side), never from user-typed input. No injection path exists.
T-05-09-04 Information Disclosure fetchDocumentContent Blob URL is same-origin and revoked on unmount. Acceptable transient exposure.
T-05-10-02 Information Disclosure OAuth URL in JSON response is a standard authorization URL containing only the CSRF state token (256-bit random). No credentials in URL.
T-05-10-04 Spoofing Nextcloud custom endpoint is pre-populated from encrypted DB credentials via the /config endpoint. Password is never returned.
T-05-11-05 Denial of Service Admin delete endpoint is already protected by admin auth + password verification. Admin accounts are trusted actors. Rate-limiting at the infrastructure level is expected.
T-05-12-03 Tampering celery-worker bind mount is developer-controlled source code. Production deployments use immutable image builds without bind mounts.

Unregistered Flags

Flag Source File Description Assessment
new-endpoint: GET /api/cloud/connections/{id}/config 05-10-SUMMARY Threat Flags backend/api/cloud.py New endpoint decrypting partial WebDAV credentials for edit modal Mitigated: get_regular_user enforced (cloud.py:680), 404 on wrong-owner (line 697-698), password field excluded from response (line 716-720), only VALID_WEBDAV_PROVIDERS accepted (line 700-703). No threat register entry needed.

Security Audit Trail

Audit Date Threats Total Closed Open Run By
2026-05-30 56 56 0 gsd-security-auditor

Sign-Off

  • All threats have a disposition
  • Accepted risks documented
  • threats_open: 0 confirmed
  • status: verified set

Approval: pending