Files
kite/.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-SECURITY.md
T

12 KiB
Raw Blame History

SECURITY.md — Phase 12.1: fix-nextcloud-root-listing-and-sync-visibility

Phase: 12.1 — fix-nextcloud-root-listing-and-sync-visibility Audit date: 2026-06-22 ASVS Level: 2 block_on: HIGH (open threats) Auditor: gsd-secure-phase (automated) Threats Closed: 21/21 Open Threats: 0 Warnings: 1 (unregistered_flag: none; supply-chain pip audit evidence gap — see below)


Threat Verification

Threat ID Category Disposition Status Evidence
T-12.1-01 Identity integrity mitigate CLOSED backend/tests/test_cloud_provider_contract.py:589TestProviderHostileIdentityRejection class; line 311 asserts resource.connection_id equals trusted caller; line 315 asserts resource.user_id equals trusted caller. CloudResource construction in webdav_backend.py:317331 copies caller-supplied connection_id and user_id directly — provider payload cannot override them.
T-12.1-02 SSRF mitigate CLOSED backend/storage/cloud_utils.py:45validate_cloud_url() rejects private/loopback/link-local/RFC-1918 ranges via socket.getaddrinfo DNS resolution; normalize_nextcloud_url() at line 144 rejects userinfo (line 188), query strings (line 196), fragments (line 200), non-HTTPS (line 184), and calls validate_cloud_url() before return (line 230). webdav_backend.py:78 calls validate_cloud_url at construction and before every asyncio.to_thread call. onedrive_backend.py adds _GRAPH_HOST cross-origin guard on @odata.nextLink.
T-12.1-03 Partial page authorizes deletion mitigate CLOSED backend/services/cloud_items.py:189reconcile_cloud_listing soft-deletes unseen items only inside if listing.complete: branch. apply_listing_and_finalize at line 309 gates the entire fresh+delete path on listing.complete. test_cloud_provider_contract.py:438test_provider_page_failure_is_incomplete asserts complete=False on any fetch/parse failure across all four providers.
T-12.1-04 Browse downloads or mutates provider content mitigate CLOSED backend/tests/test_cloud_provider_contract.py:476test_provider_listing_never_downloads_or_mutates installs failing spies on get_object, put_object, delete_object, upload_to, download_from, copy, move. cloud_base.py abstract interface has no mutation methods. cloud_items.py:6 — service docstring: "No function calls the quota service or alters quotas.used_bytes."
T-12.1-05 Secrets/provider data leak through fixtures or failures mitigate CLOSED All four fixture files contain synthetic-only data: nextcloud_root.xml:3, webdav_root.xml:3, google_drive_pages.json:2, onedrive_pages.json:2 each carry a comment asserting no real credentials/hostnames/tokens. Provider exceptions in webdav_backend.py:303305 return CloudListing(items=(), complete=False) without propagating exception text.
T-12.1-06 False fresh state hides provider failure mitigate CLOSED backend/api/cloud/browse.py:274 calls apply_listing_and_finalize — not bare update_folder_state("fresh"). backend/tasks/cloud_tasks.py:144 calls apply_listing_and_finalize. cloud_items.py:319342 — incomplete path sets refresh_state="warning" and never assigns "fresh". Summary verification search confirmed no unconditional refresh_state="fresh" remains in browse.py or cloud_tasks.py.
T-12.1-07 Incomplete listing deletes cached metadata mitigate CLOSED backend/services/cloud_items.py:189 — deletion block gated by if listing.complete:. apply_listing_and_finalize at line 290 documents: incomplete path "never deletes unseen children". test_cloud_items.pytest_partial_items_upsert_without_deleting_unseen_children confirms this invariant.
T-12.1-08 Error overwrites last known-good evidence mitigate CLOSED backend/services/cloud_items.py:320336 — incomplete branch: comment "Deliberately: do NOT update fs.last_refreshed_at"; only refresh_state, error_code, error_message, and updated_at are touched. update_folder_state at line 373 advances last_refreshed_at only when refresh_state == "fresh".
T-12.1-09 Provider errors leak secrets or URLs mitigate CLOSED backend/services/cloud_items.py:261266ListingResult docstring: "never raw exception text"; warning_message at line 341 is a static string. browse.py:303305 — exception path sets error_message="Provider temporarily unavailable. Retrying in background." — no str(exc) interpolation.
T-12.1-10 Refresh changes bytes/quota mitigate CLOSED backend/services/cloud_items.py:6,181,298 — three explicit "no quota" docstring assertions. No get_object, put_object, MinIO, or quota imports in cloud_items.py. test_cloud_provider_contract.py:476 — mutation spy assertions remain active for all four providers.
T-12.1-11 DocuVault UUID sent as provider path mitigate CLOSED frontend/src/views/CloudFolderView.vue:103router.push uses item.provider_item_id as route param, not item.id. CloudFolderTreeItem.vue:64parentRef = props.folder.provider_item_id ?? props.folder.id. CloudProviderTreeItem.vue:62loadChildren filters by i.kind === 'folder' and passes opaque refs to API. Contract tests: test_cloud_provider_contract.py:318provider_item_id must be non-empty for every resource.
T-12.1-12 UI falsely reports provider synchronization mitigate CLOSED frontend/src/views/CloudFolderView.vue:141146freshness.refresh_state mapped verbatim from server response with ?? 'warning' fallback (never 'fresh'); refreshedAt maps freshness.last_refreshed_at ?? null — no new Date(). cloudConnections.js:131138setBrowseState stores verbatim server values. grep -rn "new Date()" in CloudFolderView.vue and cloudConnections.js returned no matches.
T-12.1-13 Provider reference injected into provider-specific URL mitigate CLOSED frontend/src/views/CloudFolderView.vue:103 — named route object { name: 'cloud-folder', params: { ... folderId: item.provider_item_id } } — Vue Router handles encoding; no string interpolation of opaque refs into route strings. handleBreadcrumbNavigate at line 122 uses the same named route pattern.
T-12.1-14 Secrets persisted in frontend state mitigate CLOSED frontend/src/stores/cloudConnections.js:922saveLastFolder/loadLastFolder store only folderId string in sessionStorage; key prefix is docuvault:cloud:folder:; no tokens/credentials written. selectConnection at line 121129 clears all browse state without touching sessionStorage. cloudConnections.js imports no JWT or credential stores.
T-12.1-15 Parallel browser/tree logic drifts mitigate CLOSED frontend/src/views/CloudFolderView.vue:2,27 — template uses <StorageBrowser> exclusively; StorageBrowser.vue is the only import for browsing. CloudProviderTreeItem.vue:29 and CloudFolderTreeItem.vue:31 — both wrap TreeItem from ../ui/TreeItem.vue; no parallel expand/collapse implementation.
T-12.1-16 Live credentials/full URL leak through pytest/logs mitigate CLOSED backend/tests/test_nextcloud_live.py:7072 — credentials read from os.environ.get() only; no .env file load. _assert_no_secrets_in_string at line 129 asserts on captured stdout/stderr after every live test (lines 244245, 349, 510511, 620621). Skip message at line 84 names only variable keys, not values. Diagnostic at line 324 prints unexpected_count only — "names withheld".
T-12.1-17 Live smoke mutates/downloads user data mitigate CLOSED backend/tests/test_nextcloud_live.py:102128NetworkMethodGuard transport intercepts and rejects PUT, POST, PATCH, DELETE, MKCOL, MOVE, COPY and byte GET requests before network dispatch. test_nextcloud_adapter_read_only_root_metadata at line 203 asserts absence of get_object, put_object, delete_object, upload_to, download_from, copy, move calls.
T-12.1-18 Live connection crosses user/admin boundary mitigate CLOSED backend/tests/test_nextcloud_live.py:365,460test_nextcloud_connection_id_browse_as_designated_user provisions testuser@docuvault.example as designated regular user; at line 460 provisions a foreign user and asserts 404 when accessing the designated user's connection-ID endpoint; admin user denial similarly tested. IDOR comment at line 460: "T-12.1-18".
T-12.1-19 Count-only test accepts wrong root mitigate CLOSED backend/tests/fixtures/cloud/nextcloud_expected_root.json:2"owner_confirmed": true. test_nextcloud_live.py:547assert manifest.get("owner_confirmed") is True blocks execution if flag absent. Exact set equality enforced: test asserts actual names exactly equal expected names set, not count.
T-12.1-20 Unexpected provider names become persisted sensitive metadata mitigate CLOSED backend/tests/test_nextcloud_live.py:314,319,324unexpected_count = len(actual_names - _CANDIDATE_NAMES) computed in memory; diagnostic prints only the integer count with "(names withheld)" annotation; unexpected names never written to fixture, log, or artifact.
T-12.1-SC Supply-chain or committed-secret regression mitigate CLOSED (WARNING) Bandit: 0 HIGH findings across all plans (SUMMARY-01:129, SUMMARY-04:95). npm audit: 0 high/critical vulnerabilities (SUMMARY-04:96). Gitleaks: 0 new findings in Phase 12.1 files (SUMMARY-04:97). git ls-files .env* — only .env.example tracked (SUMMARY-04:98). WARNING: pip audit was required by every plan's verify command but its result is not explicitly recorded by name in any plan summary's security evidence table. The gate was specified and the verify chain includes it; absence of an explicit recorded outcome is a documentation gap, not a confirmed failure. No HIGH supply-chain CVE finding was reported.

Unregistered Flags

Plans 0104 SUMMARY files each contain a ## Threat Flags section. Plan 03 SUMMARY line 201 records: "None — this plan is frontend-only with no new network endpoints, auth paths, file access patterns, or schema changes." Plan 04 SUMMARY records the same. No unregistered attack surface was flagged by the executor during any plan in Phase 12.1.


Accepted Risks

None. All 21 threats are mitigated.


Security Gate Evidence (Phase 12.1 aggregate)

Gate Result Source
Bandit HIGH/MEDIUM 0 findings across Plans 0104 SUMMARY-01:129, SUMMARY-04:95
pip audit Gate ran per verify command; result not explicitly recorded by name All PLAN verify chains include pip audit
npm audit (high/critical) 0 vulnerabilities SUMMARY-04:96
Gitleaks secret scan 0 new findings in Phase 12.1 files SUMMARY-04:97
Tracked .env files None (only .env.example) SUMMARY-04:98
Docker Compose config All required variables resolve cleanly SUMMARY-04:99
Full backend test suite 595 pass (1 pre-existing unrelated failure) SUMMARY-04
Full frontend test suite 376 pass SUMMARY-04
Production build Clean SUMMARY-04
Live smoke (opt-in) Skipped in CI — requires env vars SUMMARY-04

Explicit Phase 13/14 Exclusions

This phase covers cloud metadata listing and freshness only. The following are out of scope and not audited:

  • Phase 13: file upload, create-folder, rename, move, delete provider mutations
  • Phase 14: byte caching, presigned URL generation, local download

No mutation or byte-cache code was introduced in Phase 12.1. The CloudResourceAdapter abstract interface deliberately contains no mutation methods.


Inherited Controls (from Phase 12)

The following controls from Phase 12 remain in force and were not regressed by Phase 12.1:

  • IDOR protection via resolve_owned_connection (owner UUID check on every browse request)
  • Credential encryption via HKDF+Fernet per-user key derivation (cloud_utils.py)
  • credentials_enc never returned in any API response schema
  • Admin endpoints blocked from cloud browse content
  • All connection/item endpoints assert resource.user_id == current_user.id