# SEC-008 attachment consistency verification Updated: 2026-08-02 Status: `IMPLEMENTED — NOT VERIFIED`. Repository, real-SQLite integration and isolated HTTP checks pass. Browser and production checks remain blocked. ## Revalidated execution path The former upload path copied files before validating the full batch and before the database commit; rename moved bytes before metadata commit; delete committed metadata before a best-effort file delete. These were confirmed JT-010 split-brain windows. Existing mitigations were owner-scoped queries, generated storage names, size/type/quota validation and derived attachment flags; there was no durable recovery state. The implemented invariant uses the generated final path as the operation identity: - `.uploading` is a durable staged upload. Startup promotes it when its database row exists and purges it when no row exists. - `.deleting` is quarantined deletion data. Startup restores it when its row exists and purges it when no row exists. - plain unknown files are counted and preserved; missing rows/files and unsafe stored paths are counted for review. - rename changes display metadata only. - attachment paths must remain under the configured root without child symlink/junction traversal. Transaction failure cleanup occurs only after a confirmed rollback. An uncertain commit/rollback outcome preserves the suffix marker so restart reconciliation, rather than an unsafe guess, decides from durable database state. ## Automated evidence | Check | Result | |---|---| | `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore --filter "FullyQualifiedName~AttachmentConsistencyTests|FullyQualifiedName~AttachmentFlagsRecomputeTests|FullyQualifiedName~AttachmentsControllerTests" --logger "console;verbosity=minimal"` | PASS — 20/20 | | `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore --logger "console;verbosity=minimal"` | PASS — 525/525 | | `git diff --check` | PASS — no whitespace errors; only repository line-ending notices | The real-SQLite tests cover invalid later files, cancelled copy, database rollback, upload-promotion failure and restart, delete-purge failure and restart, quarantined-delete restore, idempotent reconciliation, metadata-only rename with atomic flags, repeated same-name uploads, exact 10 MiB boundary, one-byte-over rejection, outside-root rejection, unknown-orphan preservation and two-user isolation. Failure injection uses no malicious documents. ## Runtime evidence An isolated development API ran on `127.0.0.1:5305` with disposable synthetic SQLite data: - User A login, `.txt` upload, list, download, metadata rename/purpose update and delete succeeded (`200/204` as applicable). - User B received `404` for User A's attachment download and delete. - the final owner list was empty after deletion. - startup reconciliation completed before the service accepted requests. - the exact API PID was stopped and port 5305 was confirmed closed. Logs are `docs/audits/evidence/core-001-runtime/sec-008.stdout.log` and `sec-008.stderr.log`; the source fixture is `synthetic-attachment.txt`. They contain synthetic local data only. ## Unverified gates and rollback - In-app browser access to localhost is administrator-policy blocked, so upload/rename/delete/refresh was not browser-tested. - This Windows host denied creation of a disposable symbolic link; child reparse-point refusal was code-inspected but not executed. Outside-root traversal is tested. - No production report-only orphan inventory, counter monitoring, multi-replica exercise or MariaDB runtime was performed. - Periodic reconciliation is deliberately deferred; startup retry is the current recovery trigger. - Entitlement quota behavior is owned by POL-001; the unchanged quota calculation was not reclassified as verified here. Before rollback, reconcile or manually review all `.uploading` and `.deleting` markers. Never delete unknown plain orphans automatically. No database/configuration migration was introduced.