Files
kite/.planning/phases/12-cloud-resource-foundation/12-SECURITY.md
T

12 KiB

Phase 12 Security Audit — Cloud Resource Foundation

Audited: 2026-06-22 Auditor: gsd-security-auditor (claude-sonnet-4-6) ASVS Level: 2 Plans audited: 12-01, 12-02, 12-03, 12-04, 12-05, 12-06 block_on: critical


Threat Verification

Closed Threats

Threat ID Plan Category Disposition Status Evidence
T-12-01 01/02/04 IDOR mitigate CLOSED resolve_owned_connection in backend/services/cloud_items.py:51 gates on CloudConnection.user_id == user_uuid; _get_owned_connection in connections.py:139 checks conn.user_id != user_id; get_regular_user dep on all cloud browse routes blocks admin tokens. Tests: test_cloud_security.py:test_foreign_user_cannot_browse_cloud_item, test_foreign_user_cannot_browse_subfolder, test_foreign_user_cannot_delete_connection, test_admin_cannot_browse_cloud_connection.
T-12-02 01 Same-provider account collision mitigate CLOSED _insert_cloud_connection (connections.py:109) always inserts a new UUID row — never upserts by provider. Unique constraint in migration 0006 is on (connection_id, provider_item_id) not (user_id, provider). Tests: test_cloud.py:test_same_provider_connections_are_independent (line 331) asserts two distinct UUIDs; test_cloud_security.py:test_same_provider_items_scoped_to_connection proves DB-level isolation.
T-12-03 02/04 Credential disclosure mitigate CLOSED CloudBrowseResponse, CloudItemOut, FolderFreshnessOut schemas in api/cloud/schemas.py contain no credential fields. CloudConnectionOut in api/schemas.py is used on connection-list/update endpoints. Test: test_cloud_security.py:test_browse_response_excludes_credentials_and_raw_fields asserts credentials_enc, access_token, refresh_token, client_secret absent from response body.
T-12-04 02/04 WebDAV SSRF mitigate CLOSED validate_cloud_url called at connections.py:385 (new WebDAV connection) and connections.py:533 (credential update). cloud_utils.py:validate_cloud_url blocks RFC1918, link-local (169.254.x.x), file://, and GCP metadata endpoints. Test: test_cloud_security.py:test_ssrf_url_validation_invariants (parametrized, 5 bad URLs).
T-12-05 02 Cache collision mitigate CLOSED All cloud_items.py service functions scope by connection_id: list_cloud_children (line 82), upsert_cloud_item (line 114), reconcile_cloud_listing (line 194). Browse endpoint passes cid_str = str(connection_id) to all service calls. Unique index uq_cloud_items_connection_provider_item on (connection_id, provider_item_id) prevents cross-connection collision.
T-12-06 01 Destructive capability probing mitigate CLOSED CloudResourceAdapter in storage/cloud_base.py declares only list_folder, get_capabilities, and merge_item_capabilities — no put, delete, rename, move, or create_folder methods. Frozen dataclass contract enforces no mutation. Test: test_cloud_capabilities.py fake adapter asserts no mutation method calls (29 unit tests, 12-01 SUMMARY confirms).
T-12-07 01/02 Partial-list metadata deletion mitigate CLOSED reconcile_cloud_listing in cloud_items.py:188 performs soft-deletion only under if listing.complete: guard. Failed or partial listings (complete=False) return without marking any rows deleted. Test: test_cloud_items.py:test_refresh_cloud_folder_failed_refresh_retains_cached_rows.
T-12-08 02/04 Provider error XSS/leak mitigate CLOSED Browse endpoint catch-all at browse.py:300 emits only controlled strings ("Provider temporarily unavailable. Retrying in background."); raw exceptions are never propagated. Vue template auto-escaping is active on all components (no v-html in StorageBrowser or cloud components). Input validation: malformed UUID returns FastAPI 422 before any handler runs (test_cloud_security.py:test_browse_malformed_connection_id_returns_422).
T-12-09 01/02/04 Quota corruption mitigate CLOSED cloud_items.py has no import of the quota service and no write to Quota.used_bytes. Browse endpoint has no MinIO calls. Tests: test_cloud_security.py:test_browse_no_quota_mutation (re-reads quota row after browse and asserts unchanged), test_browse_no_minio_calls (mocks get_object/put/delete and asserts not called), test_no_byte_download_during_browse (adapter byte-download methods not called).
T-12-10 03 Disabled action executes on keyboard/touch mitigate CLOSED StorageBrowser.vue:409 sets 'aria-disabled': isDisabled ? 'true' : undefined (not native disabled). CapabilityButton component suppresses click, Enter, Space, and touchend for unsupported and temporarily_unavailable states. Test: StorageBrowser.capabilities.test.js — click, Enter, Space, and touchend suppression assertions. 12-03 SUMMARY confirms 323 frontend tests pass.
T-12-11 03 Secret/token persistence mitigate CLOSED cloudConnections.js:16 stores only folderId (a provider path string) under key docuvault:cloud:folder:{connectionId} in sessionStorage. No JWT, credential field, or JSON object is stored. Test: cloudConnections.test.js:149 — "sessionStorage stores only folder references, not tokens" asserts value is a plain string, not an object.
T-12-12 03 UI source confusion mitigate CLOSED BreadcrumbBar receives connectionRoot prop with connection display name — breadcrumb root is the connection name, not a generic "Cloud" label. Provider icon appears contextually in the connection root breadcrumb only, not repeated per row. StorageBrowser adds no per-row provider badge for cloud items.
T-12-05-01 05 App starts against stale schema mitigate CLOSED docker-compose.yml migrate service runs alembic upgrade head; all application services declare condition: service_completed_successfully on migrate. Test: test_compose_migrations.py:7 passed.
T-12-05-02 05 DDL privilege escalation mitigate CLOSED migrate service receives only DATABASE_MIGRATE_URL (DDL-capable role). backend, celery-worker, celery-beat receive only DATABASE_URL (DML-only). Test: test_compose_migrations.py:test_migrate_service_uses_migrate_url.
T-12-05-03 05 Concurrent migrations mitigate CLOSED Exactly one migrate service in Compose. Application replicas have condition: service_completed_successfully — they wait, never run Alembic. Test: test_compose_migrations.py asserts exactly one service runs alembic upgrade head.
T-12-05-04 05 Secrets in Compose config or test output mitigate CLOSED Compose uses ${VAR} environment references; no rendered values in tracked files. Test: test_compose_migrations.py static assertions confirm env-var-only references.
T-12-05-05 05 Integration test destroys developer DB mitigate CLOSED test_migration_0006.py calls pytest.skip without INTEGRATION=1/INTEGRATION_DATABASE_URL. Never touches DATABASE_URL.
T-12-06-01 06 IDOR while editing/browsing multiple connections mitigate CLOSED _get_owned_connection at connections.py:132 is called before every credential update, rename, delete, and config read. Returns 404 when conn.user_id != user_id. Test: test_cloud.py:test_credential_update_wrong_owner_returns_404 (line 370). Browse path uses resolve_owned_connection (T-12-01 evidence applies).
T-12-06-02 06 Credential disclosure through multi-account list/edit/errors mitigate CLOSED update_webdav_credentials returns CloudConnectionOut.model_validate(conn).model_dump() — whitelist schema, no credentials_enc or decrypted fields. Merged credentials are written to DB only, never returned. Audit log at connections.py:571 includes only {"provider": conn.provider}, no credential data.
T-12-06-03 06 OAuth state confusion updates another connection mitigate CLOSED (by architectural exclusion) OAuth callback always calls _insert_cloud_connection (always INSERT new UUID row). There is no "reconnect existing OAuth connection" flow in Phase 12 — editing OAuth accounts is deferred to Phase 13. The threat (state causes callback to overwrite a different connection's credentials) cannot occur because the callback never targets an existing connection ID. Redis state stores only user_id with 30-minute TTL; the callback re-validates user existence and activity before proceeding.
T-12-06-04 06 SSRF bypass during credential edit mitigate CLOSED update_webdav_credentials calls validate_cloud_url(merged["server_url"]) at connections.py:533 then backend.health_check() before conn.credentials_enc = encrypt_credentials(...). No commit occurs if either check fails.
T-12-06-05 06 Cross-connection metadata collision mitigate CLOSED All service queries include both user_id and connection_id predicates (see T-12-05 evidence). Same unique constraint on (connection_id, provider_item_id) applies. Test: test_cloud_security.py:test_same_provider_items_scoped_to_connection inserts same provider_item_id under two connections and asserts two distinct rows exist.
T-12-06-06 06 Route/provider identifier confusion mitigate CLOSED CloudProviderTreeItem.vue calls getCloudFoldersByConnectionId(connection.id, '') (confirmed by 12-06 SUMMARY). CloudFolderTreeItem.vue accepts connectionId prop and uses it for all API calls and routes. Browse endpoint route /connections/{connection_id}/items is typed uuid.UUID — FastAPI rejects non-UUID slugs with 422. Test: test_cloud_security.py:test_browse_malformed_connection_id_returns_422.
T-12-SC 04/06 Supply-chain/secrets mitigate CLOSED bandit -r backend/ --severity-level medium: 0 HIGH, 0 MEDIUM (12-04 SUMMARY). npm audit --audit-level=high: 0 vulnerabilities (12-06 SUMMARY). No new Python or npm runtime packages added in Phase 12. No hardcoded secrets detected.
T-12-05-SC 05 Dependency risk from gap closure mitigate CLOSED No new runtime dependencies introduced in plan 05. Standard security gates rerun: Bandit 0 HIGH/MEDIUM, npm audit 0 high/critical.
T-12-06-SC 06 Dependency/supply-chain risk mitigate CLOSED No new runtime dependencies in plan 06. npm audit 0 vulnerabilities (12-06 SUMMARY: 334 frontend tests pass, production build clean).

Unregistered Threat Flags

The following flags appeared in SUMMARY.md ## Threat Flags sections with no formal threat ID:

Flag Source Description Assessment
credential_in_worker 12-02-SUMMARY / backend/tasks/cloud_tasks.py Credentials decrypted inside Celery worker — master key must be in CLOUD_CREDS_KEY env var on all worker nodes. Informational — not a gap. Master key from env is the established pattern (_master_key() in connections.py:59). Workers share the same Compose env. No credentials in broker payload.
sessionStorage-check 12-03-SUMMARY / cloudConnections.js saveLastFolder stores only folder path string under namespaced key. Informational — maps to T-12-11 (CLOSED).

Documentation Gap

Finding (WARNING — not a BLOCKER): The existing SECURITY.md Phase 12 threat register (written during plan 04) omits threats first introduced in plans 03 and 06: T-12-02, T-12-05, T-12-06, T-12-11, T-12-12, T-12-06-01 through T-12-06-06, T-12-05-SC, and T-12-06-SC. All mitigations are present in code; the omission is a documentation record gap, not an implementation gap. The project SECURITY.md at SECURITY.md should be extended with a "Phase 12 Plan 06 Closeout" section to close this gap.


Summary

Threats Closed: 26/26 ASVS Level: 2 Blockers: 0 Unregistered Flags: 2 (both informational, both map to closed threats or accepted architecture decisions)

All declared Phase 12 threat mitigations are present in the implementation. No BLOCKER findings. The SECURITY.md documentation record requires a supplementary section to capture threat evidence from plans 03 and 06.