Files
curo1305 73b180ac9d docs(phase-06.2): add security threat verification report
16/16 threats CLOSED — mitigate dispositions verified in code with exact file:line citations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 15:41:33 +02:00

7.2 KiB
Raw Permalink Blame History

phase, audited_at, asvs_level, threats_total, threats_closed, threats_open, result
phase audited_at asvs_level threats_total threats_closed threats_open result
06.2 2026-05-31 L1 16 16 0 SECURED

Security Audit — Phase 06.2

close-v1-sharing-cloud-delete-csv-export-gaps

Auditor: gsd-security-auditor
ASVS Level: L1
block_on: HIGH
Threats Closed: 16 / 16
Result: SECURED


Threat Verification

Threat ID Category Disposition Status Evidence
T-06.2-01-01 Tampering accept CLOSED All three xfail stubs promoted to real tests in Plans 0204; no stub logic leaked into production code paths. Acceptance rationale sound: stub-only plan cannot add attack surface.
T-06.2-02-01 Elevation of Privilege mitigate CLOSED backend/api/shares.py:264if share is None or share.owner_id != current_user.id: raise HTTPException(status_code=404, ...) in update_share_permission. Returns 404, not 403, preventing share ID enumeration. Pattern mirrors revoke_share at line 295.
T-06.2-02-02 Tampering mitigate CLOSED backend/api/shares.py:5458SharePermissionPatch.validate_permission field_validator enforces v not in {"view", "edit"} → ValueError. No arbitrary string can pass through to the ORM.
T-06.2-02-03 Tampering mitigate CLOSED backend/api/shares.py:4348ShareCreate.validate_permission applies the same {"view", "edit"} allowlist. Default "view" is server-enforced via Pydantic default; client cannot inject other values.
T-06.2-02-SC Tampering accept CLOSED Plan 02 SUMMARY confirms no new npm/pip packages installed. Acceptance rationale sound.
T-06.2-03-01 Tampering mitigate CLOSED backend/api/documents.py:654ok = await storage.delete_document(session, doc_id, skip_quota=is_cloud) where is_cloud = doc.storage_backend != "minio". backend/services/storage.py:167if not skip_quota: gates the quota decrement block. Cloud doc deletes never underflow quota.
T-06.2-03-02 Information Disclosure mitigate CLOSED backend/api/documents.py:642652except Exception as exc: catches the provider error; print(f"[cloud-delete] provider error: {exc}", file=sys.stderr) logs to stderr only; JSON response body contains only the fixed string "Cloud provider delete failed. You can remove from app only."str(exc) is never serialised into the response.
T-06.2-03-03 Elevation of Privilege accept CLOSED backend/api/documents.py:629if doc is None or doc.user_id != current_user.id: raise HTTPException(404, ...) executes before the remove_only branch at line 637638. Ownership is always asserted regardless of query param value. Acceptance rationale sound.
T-06.2-03-04 Spoofing mitigate CLOSED backend/api/documents.py:638641if is_cloud and not remove_only: block calls get_storage_backend_for_document(doc, current_user, session) which returns the cloud backend; storage.delete_document() is only reached after cloud routing is complete (or after remove_only=true skips the cloud call). MinIO is never called for cloud docs.
T-06.2-03-SC Tampering accept CLOSED Plan 03 SUMMARY confirms no new npm/pip packages installed. Acceptance rationale sound.
T-06.2-04-01 Tampering mitigate CLOSED backend/api/audit.py:216if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", date): raise HTTPException(status_code=404, ...) executes before key = f"audit-logs/{date}.csv" at line 220. Any non-date string (including path traversal sequences) is rejected with 404.
T-06.2-04-02 Elevation of Privilege mitigate CLOSED backend/api/audit.py:170list_daily_exports uses _admin: User = Depends(get_current_admin); line 205 — download_daily_export uses the same dependency. Both new endpoints require admin authentication. Regular users receive 403; unauthenticated requests receive 401.
T-06.2-04-03 Information Disclosure mitigate CLOSED frontend/src/api/client.js:398427adminExportAuditLogCsv() uses fetch() with Authorization: Bearer ${authStore.accessToken} header and res.text() → Blob → <a>.click() pattern. window.location.href is absent from frontend/src/components/admin/AuditLogTab.vue (confirmed: no match).
T-06.2-04-04 Information Disclosure accept CLOSED User handles are already public within the platform (visible in sharing UI). Admin view of handles is consistent with existing admin privileges. Acceptance rationale sound; no mitigation code is required or expected.
T-06.2-04-05 Denial of Service mitigate CLOSED backend/api/audit.py:198items = await asyncio.to_thread(_list) wraps the synchronous list_objects() iterator for list_daily_exports; line 231 — csv_bytes = await asyncio.to_thread(_get) wraps get_object() for download_daily_export. Both synchronous MinIO SDK calls are offloaded from the async event loop.
T-06.2-04-SC Tampering accept CLOSED Plan 04 SUMMARY confirms no new npm/pip packages installed. Acceptance rationale sound.

Accepted Risk Log

The following threats are accepted per plan-time decisions. No mitigation code is present or required.

Threat ID Rationale
T-06.2-01-01 Wave 0 stub plan cannot introduce production attack surface; stubs contain only pytest.xfail() calls and were verified to be fully promoted (no stubs remain) by Plans 0204.
T-06.2-02-SC No new packages installed in Plan 02. Supply-chain risk unchanged from baseline.
T-06.2-03-03 Ownership check at documents.py:629 unconditionally precedes the remove_only branch — privilege escalation via the query param is structurally impossible.
T-06.2-03-SC No new packages installed in Plan 03. Supply-chain risk unchanged from baseline.
T-06.2-04-04 User handles are public within the platform. Admin audit access to handles is consistent with the broader admin privilege model already in place.
T-06.2-04-SC No new packages installed in Plan 04. Supply-chain risk unchanged from baseline.

Unregistered Flags

None. No ## Threat Flags sections were present in any of the four SUMMARY.md files. No new attack surface was flagged by executors during implementation.


Verification Commands Run

grep -n "share.owner_id != current_user.id" backend/api/shares.py
# → lines 264, 295 (update_share_permission + revoke_share — both entry points covered)

grep -n "field_validator" backend/api/shares.py
# → lines 43, 54 (both ShareCreate and SharePermissionPatch)

grep -n "skip_quota" backend/services/storage.py
# → lines 143, 150, 167 (signature, docstring, guard)

grep -n "cloud_delete_failed\|sys.stderr" backend/api/documents.py
# → lines 644, 649 (stderr log, fixed-string response)

grep -n "re.fullmatch" backend/api/audit.py
# → line 216

grep -n "get_current_admin" backend/api/audit.py
# → lines 170, 205 (both new daily-export endpoints)

grep -n "asyncio.to_thread" backend/api/audit.py
# → lines 198, 231

grep -n "adminExportAuditLogCsv\|adminListDailyExports\|adminDownloadDailyExport" frontend/src/api/client.js
# → lines 398, 435, 460

grep "window.location.href" frontend/src/components/admin/AuditLogTab.vue
# → (no output — absent)