| T-03-01 |
Tampering |
migrations/versions/0003_multi_user_isolation.py |
mitigate |
null_user_objects collected via SELECT before DELETE; each remove_object() wrapped in try/except Exception: pass (lines 56–88) |
closed |
| T-03-02 |
Denial of Service |
Alembic migration when MinIO unreachable |
accept |
See Accepted Risks Log |
closed |
| T-03-03 |
Information Disclosure |
tests/conftest.py — xfail fixture JWTs |
mitigate |
create_access_token uses standard auth service with test secret; async_client clears dependency_overrides on teardown (line 155); no token values logged |
closed |
| T-03-04 |
Spoofing |
api/documents.py — upload-url endpoint |
mitigate |
object_key = f"{current_user.id}/{doc_id}/{uuid.uuid4()}{suffix}" computed server-side (lines 112–113); filename stored in DB only; extension from Path(body.filename).suffix.lower() |
closed |
| T-03-05 |
Tampering |
api/documents.py — confirm endpoint |
mitigate |
size = await get_storage_backend().stat_object(doc.object_key) from MinIO authoritative source (line 327); no size param in confirm body |
closed |
| T-03-06 |
Denial of Service |
api/documents.py — concurrent /confirm at quota boundary |
mitigate |
Atomic SQL: UPDATE quotas SET used_bytes = used_bytes + :delta WHERE … AND (used_bytes + :delta) <= limit_bytes RETURNING; fetchone() None → HTTP 413 (lines 341–351) |
closed |
| T-03-07 |
Information Disclosure |
Presigned URL leakage in logs |
accept |
See Accepted Risks Log |
closed |
| T-03-08 |
Repudiation |
tasks/document_tasks.py — abandoned upload orphans |
mitigate |
cleanup_abandoned_uploads Celery beat task present (lines 132–177); celery_app.py beat_schedule runs every 30 min (lines 43–46) |
closed |
| T-03-09 |
Information Disclosure |
docker-compose.yml — MinIO CORS |
mitigate |
MINIO_API_CORS_ALLOW_ORIGIN: ${FRONTEND_URL:-http://localhost:5173} — explicit non-wildcard origin (line 26) |
closed |
| T-03-10 |
Tampering |
storage/minio_backend.py — Docker hostname in presigned URL |
mitigate |
Dual MinIO client: self._client for internal ops (stat/get/delete), self._public_client for generate_presigned_put_url (lines 54–60, 154, 169) |
closed |
| T-03-11 |
Information Disclosure |
api/documents.py — cross-user doc access |
mitigate |
if doc is None or doc.user_id != current_user.id: raise HTTPException(404) at lines 322–323, 545–546, 579–580, 633–634, 702–703, 767 — returns 404 not 403 |
closed |
| T-03-12 |
Elevation of Privilege |
api/documents.py — admin reading doc content |
mitigate |
get_regular_user raises 403 for admin role (deps/auth.py:95–109); Depends(get_regular_user) on all document handlers at lines 99, 143, 302, 416, 530, 557, 613, 688, 742 |
closed |
| T-03-13 |
Information Disclosure |
api/topics.py — cross-user topic enumeration |
mitigate |
All queries filter: or_(Topic.user_id == current_user.id, Topic.user_id.is_(None)); create_topic scoped by user_id=current_user.id |
closed |
| T-03-14 |
Elevation of Privilege |
api/topics.py, api/admin.py — regular user creating system topic |
mitigate |
POST /api/admin/topics uses Depends(get_current_admin) and creates user_id=None; regular POST /api/topics forces user_id=current_user.id |
closed |
| T-03-15 |
Tampering |
api/documents.py — object_key forged with another user's UUID prefix |
mitigate |
object_key = f"{current_user.id}/{doc_id}/{uuid.uuid4()}{suffix}" — prefix always from current_user.id; no user-supplied prefix accepted |
closed |
| T-03-16 |
Spoofing |
api/documents.py — anonymous traffic |
mitigate |
HTTPBearer() with auto_error=True raises 403 on missing header (deps/auth.py:35); get_current_user raises 401 on invalid/expired token (lines 52–55) |
closed |
| T-03-17 |
Elevation of Privilege |
/api/settings endpoint |
mitigate |
backend/api/settings.py absent; main.py contains no settings_router reference — endpoint fully removed |
closed |
| T-03-18 |
Information Disclosure |
services/storage.py — settings.json flat file |
mitigate |
No load_settings, save_settings function bodies present; settings.json no longer read or written; API keys in env only |
closed |
| T-03-19 |
Tampering |
tasks/document_tasks.py — Celery task ai_provider injection |
mitigate |
Task signature is document_id: str only; user.ai_provider resolved inside _run() from DB lookup (lines 62–64) |
closed |
| T-03-20 |
Information Disclosure |
system_prompt env var in container logs |
accept |
See Accepted Risks Log |
closed |
| T-03-21 |
Repudiation |
frontend/src/views/SettingsView.vue — old API calls |
mitigate |
getSettings/patchSettings/testProvider/getDefaultPrompt absent from api/client.js; SettingsView.vue is static display only |
closed |
| T-03-22 |
Information Disclosure |
stores/documents.js — XHR PUT Authorization header |
mitigate |
Only Content-Type header set via setRequestHeader; no Authorization header in uploadToMinIO helper (line 24–25, comment cites T-03-22) |
closed |
| T-03-23 |
Spoofing |
components/upload/UploadProgress.vue — client-side quota from file.size |
mitigate |
All three values (rejected_bytes, used_bytes, limit_bytes) read from item.quotaError populated by 413 response body; no file.size used (lines 27, 30) |
closed |
| T-03-24 |
Denial of Service |
Concurrent uploads exhaust browser memory |
accept |
See Accepted Risks Log |
closed |
| T-03-25 |
Tampering |
stores/documents.js — upload state race condition |
mitigate |
const rowKey = \${file.name}__${Date.now()}`` composite key prevents collisions on duplicate filename uploads (line 70) |
closed |
| T-03-26 |
Repudiation |
stores/auth.js — quota refetch silent failure |
mitigate |
fetchQuota() wrapped in try/catch; QuotaBar.vue uses v-if="!loadFailed" to hide on error (auth.js:144–149) |
closed |
| T-03-SC |
Tampering |
Package managers (pip/npm) — all 5 plans |
mitigate |
No new pip or npm dependencies added across all 5 plans; all packages were already pinned in Phase 1/2 requirements.txt and package-lock.json |
closed |