Files
jobtrackingapp/docs/verification/sec-008-attachment-consistency.md
T

4.0 KiB

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:

  • <final>.uploading is a durable staged upload. Startup promotes it when its database row exists and purges it when no row exists.
  • <final>.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
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.