pdf-quiz-generator/docs/quiz-revamp/privacy-review.md
Daniel 1ce3eec7cb feat: authorize tutor context and protect uploaded media
Tutor questions require owned selected attempts; similarity context filters eligibility before ranking. Uploads move to a permission-aware boundary with reference ACLs, canonical legacy aliases, pre-mutation attachment checks and card-aware moderator rules. Nginx stops caching media and supplies native byte ranges. Verified 37 deployed-image backend tests, 69 frontend tests/build, real pgvector/Nginx/browser checks, and two independent reviews.
2026-09-07 15:04:00 +02:00

11 lines
No EOL
3.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Review — follow-up validation (static, read-only)
- **Finding 1 — RESOLVED.** `owns_source` (`backend/app/utils/upload_access.py:115127`) no longer treats every file as moderator-owned. The moderator grant is now `user.is_moderator and not cards and path.startswith('questions/')` (line 121), so a live card reference (`cards` non-empty from `card_deck_ids`) forces the card ACL path: `card_access` (lines 130134) requires `user_id == user.id` / `is_shared == 1` / admin — a non-owner moderator fails. Reads (`can_read_upload`, lines 137152) and attachments (`validate_image_attachments`, lines 183188) both fall through to `references()` only when the `questions/` and `images/doc_` grants do not apply, so moderator attachment of a card-only file returns 403. Admin still succeeds via `owns_source:116`. An independent question reference grants moderator access because `require_question_access` (`backend/app/utils/quiz_access.py:6990`) returns for moderators, and `validate_image_attachments:186187` uses `bool(ids)` for moderators. Covered by `test_card_only_moderator_denial_admin_success_and_question_grant` (`backend/tests/test_related_privacy.py`).
- **Finding 2 — RESOLVED.** `stored_upload_path` (`upload_access.py:6085`) canonicalizes case (`_authority` lowercases host), default port 80/443, query/fragment (dropped via `urlsplit`), percent (`unquote` on path/host), backslash (`replace('\\','/')`), and `..` (`posixpath.normpath`), and rejects anything that does not normalize back under `/uploads/` — so hostile out-of-root paths return `None`. Both classification (`references`/`card_deck_ids`/`document_for_file` compare canonicalized stored values to the route path) and permission checks use this. Anonymous access hits `get_current_user("", db)` → 401 (`uploads.py:3741`); owner success and peer 404 covered by `test_legacy_aliases_are_canonical_for_classification_and_permissions`. New-write strictness is unchanged: `validate_image_attachments:168175` rejects query/fragment, `%` in netloc, backslash, and bad ports on absolute URLs, and `local_upload_path:2431` rejects `\ % ? # :`, control chars, and `..`/empty parts. The image chooser dedupes via `stored_upload_path(row.path) or row.path` in a set (`questions.py:445447`).
- **Orphan/LMS handling — RESOLVED.** `protected` (`uploads.py:3536`) defaults non-LMS-prefix paths to private, and `document_for_file`/`questions`/`cards` force protection even inside a legacy prefix. Unreferenced `course_files|course_thumbnails|scorm|certificates` files stay anonymous (test `test_orphaned_files...`). Hostile paths can't escape: `upload_file:3447` rejects symlinks and resolves `is_relative_to(root)`; `stored_upload_path` normalizes `..` before the `/uploads/` prefix check.
- **Note (not a blocker):** `owns_source:123126` grants moderators the `images/doc_N/` branch without checking `cards`, unlike the card-aware `questions/` clause. Currently unreachable as a leak because no code path writes `Flashcard.image_path` (generation at `tasks/quiz_tasks.py:655660` sets none; `CardEdit` has only front/back), and moderators already hold document access. One-line hardening when cards get images: `if match and not cards:`.
- **Residual risks (unverified by me):** I ran no tests/commands/live DB — static only. Parent re-verified 37 backend tests, 69 frontend tests/build, Nginx 206/HEAD/416 with no-store/Vary/SVG sandbox, and pgvector eligibility-before-LIMIT; those remain parent-attested, not re-run here. Full-table scans in `references`/`card_deck_ids`/`document_for_file` per request are an acknowledged perf ceiling (`upload_access.py:88`). The `pedshub_media` cookie is not HttpOnly (deliberate, `frontend/src/utils/token.js`). No blockers remain for the two P1 findings.