## Security/Privacy review result: **BLOCK** ### Blocker 1. **Migration can commit changes and then report failure without rollback** - **File:** `scripts/health/lab_source_reconciliation.py:379-391` - The `with source:` transaction commits at line 387 before post-migration integrity and validation-digest checks run. If a trigger or unexpected schema behavior changes validation fields, the function raises `RuntimeError` after the changes are already durable. - Reproduced with a synthetic trigger: the function raised `validation fields changed`, but `canonical_document_id` and the triggered validation-field mutation remained committed. - **Remediation:** Perform updates and all postconditions inside one explicit transaction (`BEGIN IMMEDIATE`), rollback on every failed check, and commit only after integrity, foreign-key, exact changed-row count, validation digest, and intended-column checks pass. Ideally validate against a copied database before touching production. ### High 2. **“Private” report writer follows symlinks, permits repository destinations, and does not enforce `0600` on existing files** - **File:** `scripts/health/lab_source_reconciliation.py:346-350` - `os.open(... O_CREAT | O_TRUNC, 0o600)` follows symlinks and only applies `0600` when creating a new inode. An existing `0644` report remains `0644`. `--private-report` and `--private-staging-plan` may also point inside the Git repository. - Reproduced both behaviors: an existing file stayed mode `0644`, and a symlink target was overwritten. - **Remediation:** Require canonical containment beneath a dedicated private root outside the repository; reject symlinks and non-regular targets; use `O_NOFOLLOW | O_EXCL`, write to a newly created `0600` temporary file, `fsync`, and atomically rename. Verify final owner and mode. Add tests for existing permissive files, symlinks, repository paths, and concurrent filename collisions. 3. **Full-PHI database backups can be created as `0644`, left exposed on failure, or written into the repository** - **File:** `scripts/health/lab_source_reconciliation.py:363-377,404` - SQLite creates the backup before `chmod(0600)`, and chmod occurs only on the success path. A post-migration failure leaves the complete database backup at ambient permissions. `--backup-dir` is unrestricted. - Reproduced: after the validation failure above, the backup remained mode `0644`. - **Remediation:** Restrict backups to a canonical private directory outside Git, reject symlinked directories, pre-create the destination exclusively with `0600`, and preserve that mode throughout. Secure or remove incomplete backups in every exception path. ### Medium 4. **Document coverage bypasses the reviewed-original gate and opens unreviewed originals** - **Files:** - `scripts/health/dashboard_v5/read_api.py:1861-1870` - `scripts/health/lab_source_reconciliation.py:152-157,275` - Both paths pass `reviewed=True` unconditionally. `probe_original()` consequently opens each original and reads its magic bytes even when `review_status` is not `geprueft`, bypassing the established “reviewed originals only” boundary. - **Remediation:** Pass the actual review state. Return `not_checked` for unreviewed documents, or introduce a separately reviewed, explicitly authorized offline metadata probe that cannot expose or stream content. Do not silently override the existing review gate. 5. **The documented “restore test” is only an integrity check of the backup** - **Files:** - `scripts/health/lab_source_reconciliation.py:399-405` - `docs/sprint6g-a2-laboratory-reconciliation.md:80` - Opening the backup read-only and running `PRAGMA integrity_check` does not test restoration or verify that it represents the exact pre-migration state. - **Remediation:** Restore the backup into a separate temporary database, run integrity and foreign-key checks there, and compare a pre-migration logical/schema digest. Rename the current result to `backup_integrity_check` until an actual restore exercise exists. ## Passed checks - Focused tests: `7 passed`. - API duplicate query parameters fail closed with HTTP 400 / `duplicate_parameter`. - Explorer and record URL parsers reject duplicate controlled parameters. - No prohibited raw PHI was found in the new versioned Sprint 6G-A.2 document; it contains aggregate counts, contract/status names, and digests. - Exact-link candidate generation does not use filename, date, institution, OCR text, or fuzzy matching. - `git diff --check` passed. ## Review hygiene - **Files created or modified by me:** none. - A concurrent change to `tests/browser/dashboard_v5.spec.js` appeared during the review; I inspected it and found only locator disambiguation, with no effect on these findings.