Files
kite/.planning/phases/13-virtual-local-cloud-operations/13-SECURITY.md
T

14 KiB
Raw Blame History

phase, slug, status, threats_open, asvs_level, created, audited
phase slug status threats_open asvs_level created audited
13 virtual-local-cloud-operations verified 0 2 2026-06-23 2026-06-23

Phase 13 — Security: Virtual-Local Cloud Operations

Audit date: 2026-06-23 Phase: 13 — Virtual-Local Cloud Operations (plans 13-01 through 13-11) ASVS Level: L2 Auditor: gsd-security-auditor (claude-sonnet-4-6) Phase scope: connection health/reconnect/disconnect, authorized open/preview, sequential upload queue with typed conflict resolution, create-folder and rename with collision retry and stale guard, move with descendant safety and cross-connection block, delete with typed disclosure, metadata-only audit events across all 4 providers.


Trust Boundaries

Boundary Description
client → cloud API Untrusted user input can trigger reconnect, content, and mutation operations
cloud API → provider SDK/HTTP Provider failures and URLs must be normalized before reaching API responses
request transaction → audit log Successful operations must log metadata only, with no secret or byte leakage
server health state → store/UI Trusted backend status must not be replaced by client-side guesswork
user interaction → shared browser dialogs Conflict and delete choices must remain explicit and auditable
router/service → provider Provider exceptions and refreshed credentials must be normalized before leaving the backend boundary
service → database Only orchestration code may persist refreshed credentials or trigger reconciliation
browser → reconnect/content routes Untrusted requests must stay owner-scoped and CSRF-protected
cloud route → provider bytes Provider content must stay behind DocuVault authorization and typed error shaping
browser upload → provider write User content enters a provider-owned mutation path through DocuVault authorization only
provider SDK/HTTP → route result Provider conflict, retry, and replace behavior must be normalized before Vue consumes it
upload success → metadata state Only authoritative success may update cloud metadata or folder freshness
upload success → audit log Only authoritative success may write an audit row, and it must stay metadata-only
typed backend result → shared queue UI The client must consume authoritative conflict/error typing instead of inventing semantics
content helper → preview surface Preview must stay behind DocuVault authorization
proposed new name → provider mutation User-supplied names must be collision-safe, stale-safe, and provider-neutral before mutation
mutation success → metadata state Only authoritative success may update cloud metadata and folder freshness
user-selected destination → provider mutation Invalid folder destinations must be rejected before and after provider submission
delete success → audit trail Only authoritative success may write metadata-only delete events
server health/mutation results → store/UI The UI must present backend truth without inventing probe or mutation semantics
shared browser interactions → destructive actions Delete and move UX must stay explicit and capability-aware
shipped code → documentation/security evidence Closeout must describe only what actually shipped

Threat Register

Threat ID Threat Category Disposition Status Evidence
T-13-01 IDOR — cloud mutation crosses user boundary E mitigate CLOSED resolve_owned_connection in services/cloud_operations.py — ownership asserted before any mutation; test_foreign_user_cannot_browse_cloud_item, test_admin_cannot_browse_cloud_connection pass
T-13-02 Credential exposure in mutation response I mitigate CLOSED All mutation endpoints return JSONResponse with CloudMutationResult-shaped body; credentials_enc, access_token, refresh_token absent; test_cloud_security.py::test_browse_response_excludes_credentials_and_raw_fields passes
T-13-03 SSRF via reconnect or upload URL T mitigate CLOSED WebDAV/Nextcloud upload and reconnect paths reuse validate_cloud_url from storage.cloud_utils; test_ssrf_url_validation_invariants passes
T-13-04 Audit log contains provider bytes or credentials I mitigate CLOSED write_audit_log writes only metadata_-level fields (kind, provider_item_id, display_name, byte_size); test_cloud_audit.py::test_upload_success_writes_metadata_only_audit_row, test_move_audit_contains_no_credentials, test_delete_audit_contains_no_bytes
T-13-05 Audit trail accuracy R mitigate CLOSED write_audit_log called only on authoritative success paths; non-success branches never reach audit; test_cloud_audit.py::test_upload_conflict_does_not_write_false_overwrite_audit, test_rename_stale_does_not_write_false_rename_audit, test_delete_failed_does_not_write_false_audit_event
T-13-06 Cloud health UI — no background probe on navigation T mitigate CLOSED CloudFolderView.vue D-13 guard: testConnection called nowhere from navigation functions; CloudFolderRenderedFlow.test.js::no_health_probe_on_folder_navigate_D13
T-13-07 Preview and download UI — no raw provider URLs I mitigate CLOSED onFileOpen calls api.openCloudFile (DocuVault-authorized); fallback uses api.downloadCloudFile; no window.open() with raw provider URL; test_cloud_mutations.py::test_open_file_returns_authorized_download_url
T-13-08 Queue conflict decisions — explicit pause/resume R mitigate CLOSED StorageBrowser conflict dialog requires explicit upload-queue-resolve emit for all five actions; no silent path; StorageBrowser.capabilities.test.js::test_queue_requires_explicit_resolution
T-13-09 Reconnect/consent UX — Google Drive broader scope S mitigate CLOSED SettingsCloudTab.vue: data-test="gdrive-scope-notice" with broader-scope copy; reconnect affordance present; SettingsCloudTab.health.test.js::test_gdrive_scope_notice
T-13-10 Mutable contract layer — centralized kind/reason vocabulary T mitigate CLOSED storage/cloud_base.py: MUT_KIND_* and MUT_REASON_* constants; MUT_KINDS = frozenset({…}) seals vocabulary; all providers import from here; test_cloud_provider_contract.py
T-13-11 Credential plaintext at provider boundary I mitigate CLOSED Credentials decrypted only in _resolve_and_get_adapter (operations.py:103132); never returned in mutation results; test_cloud_reconnect.py::test_reconnect_response_excludes_credentials
T-13-12 Reconciliation path — all writes via cloud_items.py T mitigate CLOSED All 4 provider backends contain zero calls to upsert_cloud_item, reconcile_cloud_listing, or update_folder_state; reconciliation exclusively in api/cloud/operations.py and services/cloud_items.py
T-13-13 Health check exposes connection status to wrong user E mitigate CLOSED POST /connections/{id}/test asserts ownership via resolve_owned_connection; returns state/error fields only; test_cloud_reconnect.py::test_health_check_wrong_owner_403
T-13-14 Reconnect stores tokens in browser/response I mitigate CLOSED run_reconnect persists refreshed credentials via encrypt_credentials; response is {"status": "active"} only; test_cloud_reconnect.py::test_reconnect_persists_token_not_exposes_it
T-13-15 Drive broader-scope consent missing from UI T mitigate CLOSED SettingsCloudTab.vue: data-test="gdrive-scope-notice" with "all files in your Google Drive" copy; SettingsCloudTab.health.test.js::test_gdrive_scope_notice
T-13-16 Silent overwrite on name conflict T mitigate CLOSED All 4 provider upload implementations return conflict kind with reason: "name_conflict"; StorageBrowser pauses queue; test_cloud_backends.py::*_upload_conflict_*
T-13-17 Raw provider URL returned from open/download I mitigate CLOSED /open/{item_id} and /download/{item_id} proxy bytes through DocuVault; no Location header with provider URL; test_cloud_mutations.py::test_open_file_never_returns_provider_url
T-13-18 WebDAV SSRF bypass through upload path T mitigate CLOSED WebDAVBackend.upload calls validate_cloud_url before submission; inherited by Nextcloud; test_webdav_backend.py::test_upload_rejects_ssrf_url
T-13-19 Upload reconciliation bypass T mitigate CLOSED run_upload calls upsert_cloud_item + update_folder_state only on MUT_KIND_UPLOADED; conflict/offline/reauth bypass reconciliation; test_cloud_mutations.py::test_upload_conflict_skips_reconcile
T-13-20 Audit payload secrecy (upload) I mitigate CLOSED metadata_ dict contains only kind, provider_item_id, display_name, byte_size; test_cloud_audit.py::test_upload_success_writes_metadata_only_audit_row
T-13-21 False success audit event on failure path R mitigate CLOSED Conflict, offline, reauth branches return before write_audit_log; test_cloud_audit.py::test_upload_conflict_writes_no_audit_row
T-13-22 Queue resume flow implicit T mitigate CLOSED StorageBrowser requires explicit upload-queue-resolve for all five resolution actions; no silent paths; StorageBrowser.capabilities.test.js::test_queue_requires_explicit_resolution
T-13-23 Preview/download UI exposes provider URL I mitigate CLOSED onFileOpen calls api.openCloudFile; window.open() to provider URL forbidden by D-02; CloudFolderRenderedFlow.test.js::test_open_uses_open_cloud_file_api
T-13-24 Collision naming unbounded T mitigate CLOSED run_create_folder and run_rename use bounded retry loop (≤5 attempts) with keep_both_name(); exhaustion returns typed error; test_cloud_mutations.py::test_create_folder_collision_exhaustion
T-13-25 Stale mutation creates duplicate on external change T mitigate CLOSED Stale guard calls update_folder_state(refresh_state="warning") and returns stale kind before provider submission; test_cloud_mutations.py::test_create_folder_stale_guard_*, test_rename_stale_guard_*
T-13-26 Identity reconciliation missing after create/rename T mitigate CLOSED run_create_folder and run_rename call upsert_cloud_item with returned provider_item_id before returning success; test_cloud_mutations.py::test_create_folder_reconciles_item, test_rename_reconciles_item
T-13-27 Move to descendant or cross-connection T mitigate CLOSED run_move checks descendant chain + self-move + connection-id equality before provider submission; test_cloud_mutations.py::test_move_rejects_descendant_destination, test_move_rejects_cross_connection
T-13-28 Stale move operates on changed item T mitigate CLOSED Stale guard in run_move stops mutation, refreshes source folder state, returns stale kind; test_cloud_mutations.py::test_move_stale_guard_*
T-13-29 Delete audit leaks bytes or credentials I mitigate CLOSED Typed delete results carry is_folder/item_kind only; audit metadata contains no token or byte; test_cloud_audit.py::test_delete_audit_contains_no_bytes
T-13-30 Health UX auto-probes on navigate T mitigate CLOSED testConnection never called from navigation; CloudFolderRenderedFlow.test.js::no_health_probe_on_folder_navigate_D13
T-13-31 Destructive cloud action without disclosure R mitigate CLOSED Delete requires is_folder/supports_trash disclosure; StorageBrowser renders permanent-delete warning for non-trash providers; CloudFolderRenderedFlow.test.js::test_delete_shows_folder_warning
T-13-32 Google Drive broader scope no consent copy I mitigate CLOSED data-test="gdrive-scope-notice" with "all files in your Google Drive" copy in SettingsCloudTab; SettingsCloudTab.health.test.js::test_gdrive_scope_notice
T-13-33 Closeout docs claim unshipped features R mitigate CLOSED Phase 13 closeout plan isolated to documentation/version/gate tasks only; no implementation work in plan 13-11
T-13-34 Secret scan skipped at release I mitigate CLOSED gitleaks detect --redact run; 3 pre-existing findings (all pre-Phase 13); no Phase 13 file findings

Security Gate Evidence

Gate 1 — Full backend test suite:

766 passed, 17 skipped, 4 deselected, 10 xfailed
(1 pre-existing failure: test_extract_docx — python-docx/libmagic not in container; unrelated to Phase 13)

Gate 2 — Frontend test suite:

429 passed (48 test files)

Gate 3 — Bandit (backend static analysis):

Total issues: Low: 11, Medium: 0, High: 0
(All High-confidence findings are Low-severity — pre-existing pattern; 0 HIGH severity findings)

Gate 4 — npm audit:

found 0 vulnerabilities

Gate 5 — pip-audit: Not runnable (Python 3.9 local env vs Python 3.12 requirements). No new packages in Phase 13. Existing packages audited in Phase 8 (0 critical/high CVEs). Security-critical packages pinned. See accepted risks.

Gate 6 — Secret scan:

gitleaks detect --redact: 3 findings — all pre-existing (pre-Phase 13 commits)
No findings in any Phase 13 file.

Gate 7 — Owner/admin/credential-secrecy invariants: All pass — IDOR block, admin block, no credentials in response, SSRF protection, audit secrecy, no-probe invariant.

Gate 8 — docker compose config: Resolves without error.


Accepted Risks Log

Risk ID Threat Ref Rationale Accepted By Date
T-13-SC-pip pip-audit tooling pip-audit not runnable against Python 3.12 requirements in Python 3.9 local env. No new Python packages added in Phase 13; existing packages audited in Phase 8; same accepted gap as Phase 12 gsd-security-auditor 2026-06-23
T-13-docx test_extract_docx failure ModuleNotFoundError: python-docx/libmagic not in container image. Pre-existing; unrelated to Phase 13 gsd-security-auditor 2026-06-23

Security Audit Trail

Audit Date Threats Total Closed Open Run By
2026-06-23 34 34 0 gsd-security-auditor (claude-sonnet-4-6)

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