EVO-1 ·
evolutivaapi-images tenant isolation: key->tenant binding, scoped serve/delete, per-tenant storage + stats, _trash TTL, server-side EXIF strip, backup (closes cross-tenant IDOR)
- Ref
EVO-1(#982)- Project
evolutiva- Status
- done
- Priority
- high
- Type
- feature
- Assigned
- coder-apiimages-cc coder
- Created by
- wi-cli-venus
- Created
- 2026-06-13T07:01:36.150Z
- Updated
- 2026-06-13T08:29:05.534Z
- Closed
- 2026-06-13T08:29:05.534Z
Questions
No questions.
Event log
-
Spec S1-S8 (greenlit by Elazar via aro:evolutiva-management, unanimous 3 PMs + coder): S1 key->tenant binding (env API_KEY_<TENANT> -> tenant id); serve/delete scoped to caller's tenant. S2 new uploads -> uploads/{tenant}/{year}/{id}.jpg; trash -> uploads/_trash/{tenant}/{year}/. S3 legacy read-fallback: serve/delete try tenant path then legacy flat uploads/{year}/. Keeps bare YYYY_uuid ids resolvable, no app DB change. S4 backfill MIGRATION: move legacy flat files into tenant dirs using each app's owned id list (mars: práctica refs; pluto: users.profileImageId+documentoImageId via db-pluto-cc; venus: near-empty). S5 GET /stats per-tenant (bytes+count) -> unblocks MARS-42, MARS-390. S6 _trash 90-day API-owned purge (cron or on-write sweep); Elazar can override window. S7 lossless server-side EXIF strip on upload: imagick stripImage() if available else raw JPEG APP1-segment strip; NEVER GD re-encode (radiograph quality). JPEG-only kept, no PNG. S8 backup: uploads tree currently UNBACKED. Likely os/infra owner (rclone->gdrive or Hostinger backup), not the PHP API. Scope options, surface owner question to Elazar. SECURITY NUANCE (coder finding, flag to PMs): S3 read-fallback ALONE does NOT close the IDOR for pre-cutover images — legacy flat dir has no tenant marker, so any tenant key can read any legacy id via fallback. Only S4 backfill migration fully closes legacy exposure. Therefore S4 is REQUIRED for the security goal, not optional/lazy. Mitigations during transition: UUIDv4 ids are unguessable + keys are server-side, so practical exposure is low; migration closes it fully. Option to gate fallback by an ownership manifest from backfill lists for zero transitional exposure.
-
Building on branch; will report diff for review before deploy.
-
Spec written to repo: agents/tenant-isolation-spec.md (branch feat/tenant-isolation). Includes S1-S8 + the legacy-IDOR security nuance (S4 backfill required, not optional, to close legacy exposure).
-
Built + tested + pushed to branch feat/tenant-isolation (PR: https://github.com/ElazarPimentel/api-images-evolutiva/pull/new/feat/tenant-isolation). NOT deployed (Hostinger manual, gated on review). Tests: 21 unit (tenant validation, path resolution, cross-tenant isolation, lossless EXIF strip incl entropy-data preservation) + 10 e2e through router (auth/401, upload->uploads/{tenant}/{year}/, serve 200+decodes, cross-tenant serve/delete 404, stats per-tenant, soft-delete->trash, legacy-flat fallback 200) + CLI tools (purge 90d cutoff, backfill move) all green. Files: inc/storage.php, stats.php, tools/backfill.php, tools/purge-trash.php new; inc/auth.php (require_api_key returns tenant), upload/serve/delete.php, index.php (+/stats route), inc/image.php (strip_jpeg_metadata), .htaccess (block ^tools/), .env.example (API_KEY_VENUS) modified.
-
Backfill coordination (all 3 PMs ready): generate each tenant's owned-id list FRESH at execution time (post-deploy), NOT now, so uploads between now and cutover aren't orphaned from the list (mars/MARS-110 point, applies to all). Order: pluto first (ID-document PII), then venus (gated on coder-venus live-count+uniqueness), then mars (db-mars same-day). Sequence per tenant: verify imagick+dirs on box -> pull fresh id list -> backfill --tenant=X dry-run -> review count w/ PM -> --apply. Legacy IDOR stays open per-tenant until its backfill runs (acknowledged by all).
-
audit-pluto review (commit 9b6c3d4): NEW-upload isolation/key-binding/traversal/stats all PASS. Fixed HIGH (dropped lossy imagick branch -> always lossless raw strip), MEDIUM (legacy fallback now READ-ONLY: delete is tenant-path-only, closes destructive cross-tenant legacy delete), LOW (broadened strip to APP1-APP15+COM, tightened id anchors to \z, CLI-only guard on tools/). Re-tested: 11 unit + e2e (legacy cross-tenant delete now 404 + file preserved, legacy serve still 200, own+post-backfill delete work). Pushed 8b32dd6. mars-audit also PASS (ec727f8). Awaiting audit re-verify.
-
Done — live in prod (on-box md5=b6ea058), 3 audits PASS, smoke 11/11, IDOR closed all 3 tenants (legacy incl). key→tenant binding + per-tenant storage uploads/{tenant}/{year}/, scoped serve/delete, /stats+disk warning, lossless EXIF strip, daily self-gating purge, uploads-safe deploy. Backfill: pluto 10, mars 5690 (0 err), venus no-op.