From fccb9c63944bd5abd38ccc224e8c0d2035105474 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Fri, 19 Jun 2026 01:52:49 +0200 Subject: [PATCH] security(12-04): add Phase 12 security gate evidence and cloud ops runbook - SECURITY.md: Phase 12 threat register (T-12-01 through T-12-SC) with evidence for all 8 threat IDs; bandit/npm audit gate results; accepted risks for pip-audit tooling gap and DISABLED connection behavior - RUNBOOK.md: Phase 12 cloud operations section covering connection management, browse refresh lifecycle, item metadata queries, stuck-refresh recovery, and security operation notes --- RUNBOOK.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++ SECURITY.md | 59 +++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/RUNBOOK.md b/RUNBOOK.md index 1be9173..3a4cbf0 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -567,6 +567,94 @@ malformed. --- +## 9. Phase 12 — Cloud Resource Foundation Operations + +### Cloud connection management + +**List a user's cloud connections (admin):** +```bash +# Via API (admin token required for admin panel only — not cloud browse endpoints) +curl -H "Authorization: Bearer $ADMIN_TOKEN" http://localhost:8000/api/admin/users +# Cloud connections are user-scoped. Admins cannot browse connection items. +``` + +**Check connection status from DB:** +```sql +SELECT id, user_id, provider, display_name, status, created_at +FROM cloud_connections +WHERE user_id = ''; +``` + +**Re-enable a DISABLED connection:** +```sql +UPDATE cloud_connections SET status = 'ACTIVE' WHERE id = ''; +``` + +### Cloud browse background refresh + +The browse endpoint (`GET /api/cloud/connections/{id}/items`) serves cached metadata and schedules a background refresh when stale: +- **Freshness states:** `fresh` (just refreshed), `refreshing` (refresh in progress), `stale` (last refresh > threshold), `warning` (refresh failed) +- **Cached data is always served** — the UI shows a warning banner for stale/error states without blocking navigation +- **No bytes are downloaded** during browse; `size_bytes` values come from provider-reported metadata only + +**Trigger a manual refresh (no direct endpoint — use the UI Refresh button):** +The UI CloudFolderView sends a refresh request that updates `cloud_folder_states.refresh_state` to `refreshing` and schedules a background task. + +**Inspect refresh state:** +```sql +SELECT connection_id, folder_ref, refresh_state, refreshed_at, error_code +FROM cloud_folder_states +ORDER BY refreshed_at DESC NULLS LAST +LIMIT 20; +``` + +**Clear stuck refreshing state:** +```sql +UPDATE cloud_folder_states +SET refresh_state = 'stale', error_code = NULL +WHERE refresh_state = 'refreshing' + AND refreshed_at < NOW() - INTERVAL '15 minutes'; +``` + +### Cloud item metadata + +Cloud items are stored in `cloud_items`. They are metadata-only — no file bytes are in the database. + +**Count items per connection:** +```sql +SELECT connection_id, count(*) AS item_count +FROM cloud_items +GROUP BY connection_id; +``` + +**Purge orphaned items (after connection deleted):** +```sql +-- cloud_items.connection_id has ON DELETE CASCADE — orphaned items are removed automatically +-- Verify with: +SELECT count(*) FROM cloud_items ci +LEFT JOIN cloud_connections cc ON ci.connection_id = cc.id +WHERE cc.id IS NULL; +-- Should return 0 +``` + +### Security operations — cloud browse + +- **IDOR protection:** All browse endpoints use `get_regular_user` and assert `connection.user_id == current_user.id`. Violations return 404, not 403, to avoid enumeration. +- **Admin tokens:** Blocked by `get_regular_user`. Admin users cannot browse other users' connections. +- **Credential fields:** `credentials_enc` is never included in browse API responses. Verify with: `grep -r "credentials_enc" backend/api/cloud/` — must only appear in connection create/update service code, never in response schemas. +- **SSRF guard:** `storage/cloud_utils.validate_cloud_url` blocks RFC1918, link-local, file://, and cloud metadata IPs. All WebDAV/Nextcloud URLs pass through this guard before any connection is established. + +### Phase 12 Deferred Items + +| Item | Current state | Future path | +|------|---------------|-------------| +| **Cloud file upload/move/delete** | Browse is metadata-only. Write operations return `temporarily_unavailable`. | Phase 13 will implement cloud mutations (upload, move, delete) with full quota integration. | +| **Byte-level caching** | Size metadata is shown; file bytes are never fetched during browse. | Phase 14 will implement a byte cache for offline/preview access. | +| **Real-time push refresh** | Background refresh is poll-based via the UI Refresh button. | Future: WebSocket or SSE push from background task completion. | +| **pip-audit in CI** | pip-audit not in local Python 3.9 environment. | Add `pip-audit` to requirements-dev.txt and CI pipeline when moving to Python 3.12. | + +--- + ## 8. Phase 6 Deferred Items The following improvements are out of scope for Phase 6. They are documented here so the diff --git a/SECURITY.md b/SECURITY.md index 3823015..f9cd6d2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -326,3 +326,62 @@ None. SUMMARY.md files for plans 08-01 through 08-08 report no new attack surfac | T-08-06-SC | No new packages | Plan 08-06 adds no new pip or npm packages | Auth decomposition is a refactor of existing code | | T-08-07-SC | No new npm packages | Plan 08-07 adds no new npm packages | API client decomposition uses existing fetch/Pinia stack | | T-08-08-03 | requirements.txt version pinning | Pinned versions are visible in requirements.txt | Reproducibility benefit (D-17) outweighs version disclosure; versions are not secrets; no credentials | + +--- + +## Phase 12 — Cloud Resource Foundation (2026-06-19) + +### Phase 12 Threat Register + +| Threat ID | Threat | Disposition | Status | Evidence | +|-----------|--------|-------------|--------|----------| +| T-12-01 | IDOR / admin content access to cloud items | mitigate | CLOSED | `backend/tests/test_cloud_security.py` — `test_foreign_user_cannot_browse_cloud_item` (404 IDOR block), `test_admin_cannot_browse_cloud_connection` (403/404 admin block); `api/cloud/browse.py` — `get_regular_user` dep blocks all admin tokens | +| T-12-03 | Credential disclosure in browse response | mitigate | CLOSED | `backend/tests/test_cloud_security.py` — `test_browse_response_excludes_credentials_and_raw_fields` asserts `credentials_enc`, `access_token`, `refresh_token`, `client_secret` absent; `backend/tests/test_cloud.py` — `test_credentials_enc_not_exposed` for connection list endpoint | +| T-12-04 | SSRF via user-supplied WebDAV/Nextcloud URL | mitigate | CLOSED | `backend/tests/test_cloud_security.py` — `test_ssrf_url_validation_invariants` covers link-local, RFC1918, file://, metadata endpoints; `storage/cloud_utils.py:validate_cloud_url` — unchanged from Phase 5 | +| T-12-07 | Metadata loss on failed provider refresh | mitigate | CLOSED | `backend/tests/test_cloud_items.py` — `test_refresh_cloud_folder_failed_refresh_retains_cached_rows` asserts cached rows survive failed refresh | +| T-12-08 | XSS / raw provider error leakage | mitigate | CLOSED | `api/cloud/browse.py` — controlled error messages (`"Provider temporarily unavailable. Retrying in background."`) never raw exceptions; Vue template auto-escaping active in all components | +| T-12-09 | Quota corruption from browse (no-byte invariant) | mitigate | CLOSED | `backend/tests/test_cloud_security.py` — `test_browse_no_quota_mutation`, `test_browse_no_minio_calls`, `test_no_byte_download_during_browse`; `backend/tests/test_cloud_items.py` — `test_refresh_cloud_folder_no_byte_calls` | +| T-12-10 | Disabled control action executed by pointer/keyboard/touch | mitigate | CLOSED | `frontend/src/components/storage/__tests__/StorageBrowser.capabilities.test.js` — aria-disabled click, Enter, Space, and touchend all suppressed for unsupported/temporarily_unavailable states | +| T-12-SC | Supply-chain and committed secrets | mitigate | CLOSED | `bandit -r backend/ --severity-level medium`: 0 HIGH, 0 MEDIUM findings. `npm audit --audit-level=high`: 0 vulnerabilities. No new packages introduced in Phase 12. | + +### Phase 12 Security Gate Evidence + +**Gate 1 — Bandit (backend static analysis):** +``` +bandit -r backend/ --severity-level medium --confidence-level medium -x tests/ +No issues identified. +High: 0, Medium: 0 +``` + +**Gate 2 — npm audit (frontend dependency scan):** +``` +npm audit --audit-level=high +found 0 vulnerabilities +``` + +**Gate 3 — pip-audit:** +pip-audit was not installed in the local Python 3.9 environment. No new Python dependencies were added in Phase 12 (no new entries in `requirements.txt`). All existing dependencies were audited in Phase 8 with 0 critical/high CVEs. This is a local tooling gap, not a project-level vulnerability. + +**Gate 4 — Security-invariant tests:** +``` +backend/tests/test_cloud_security.py: 16 passed +backend/tests/test_cloud.py (browse security subset): all passing +Full backend suite: 509 passed, 1 pre-existing failure (test_extract_docx — missing libmagic, not Phase 12) +``` + +**Gate 5 — Admin content access:** +Admin tokens are blocked by `get_regular_user` dependency on all cloud browse and connection endpoints. Verified by `test_admin_cannot_browse_cloud_connection` (returns 403/404). No admin endpoint returns `credentials_enc`, cloud file bytes, or document content. + +**Gate 6 — No committed secrets:** +Phase 12 adds no `.env` files, no hardcoded tokens, and no API keys. Credential encryption uses `CLOUD_CREDENTIAL_KEY` from env only. Session storage stores folder path strings only (verified by `test_session_storage_value_is_not_json_object` in cloudConnections tests). + +### Phase 12 Unregistered Flags + +None. Phase 12 adds a read-only cloud browse surface (no MinIO writes, no quota mutations). The session-storage folder path flag was captured in Phase 03 SUMMARY.md with evidence. + +### Phase 12 Accepted Risks + +| Risk ID | Component | Accepted Risk | Rationale | +|---------|-----------|---------------|-----------| +| T-12-SC | pip-audit tooling | pip-audit not installed in local Python 3.9 environment | No new Python packages added in Phase 12; existing packages audited in Phase 8. Operator should run pip-audit in CI or with Python 3.12 target environment. | +| T-12-disabled | DISABLED connection returns 200 with unavailable capabilities | Browse endpoint is accessible but all operations are unavailable | This is correct behavior: ownership is still asserted (user owns the connection), and capabilities convey the unavailability. No cloud bytes or credentials are served. |