TC-EDGE-003: Upload Sanitization & Storage

🆕 New on stage — 2026-06-25 → 2026-07-01. This TC covers the canonical upload-hardening wave: filename sanitization on every upload (sanitizeUploadFilename, #444/G16), bare-S3-key storage + read-time presign for logos/avatars/proofs (#451/#348/#406/#407), and the external-avatar-URL redirect guard (#410) that stops the image-redirect service from mangling a member's external avatar link. See the Test Plan and the Jun 25 release / Jul 1 release notes.

Module: Edge Cases — Upload Sanitization & Storage

Primary Test User: admin@rcme.membervu.com / Admin123!; testmember@rcme.membervu.com / Member123! for avatar/proof upload; treasurer@demo.membervu.com / Treasurer123! for payment-proof review

Priority: P1 (path-traversal / header-injection hardening on a public upload surface)

Objectives

  1. Upload a file whose name is a path-traversal / control-char / formula-injection payload and confirm the platform neutralizes it — never 500s, never stores the raw hostile string.
  2. Confirm avatars/logos/payment proofs are persisted as a bare S3 key (not a full or presigned URL) and resolve to a working image for an anonymous/authorized viewer via a fresh presigned URL on every read.
  3. Confirm an org logo saved before this hardening (a legacy full-URL value) still renders after the deploy — no backfill migration was run.
  4. Confirm a member's external avatar URL (e.g. an outside image host) is passed through untouched, not rewritten into a broken redirect-service link (#410).

Scenario summary

IDScenarioKey data assertion
EDGE3-01Path-traversal filename (../../etc/passwd)Upload succeeds (no 500); the Upload.originalFilename audit row is sanitized to passwd.bin — no / or .. survives.
EDGE3-02Control-char / whitespace filenameUpload succeeds; sanitized name is ASCII-only, no tabs/newlines/control bytes, extension preserved.
EDGE3-03Formula-injection-style filename (=cmd|'/c calc'!A0.png)Upload succeeds; sanitized name has no leading = and no |/'/! — e.g. c_calc_A0.png.
EDGE3-04Avatar/logo/proof stored as bare keyDB column (Member.avatarUrl / OrgProfile.logoUrl / Payment.proofUrl) holds a bare storage key like avatars/<tenant>/<member>/avatar-
jpg — not a full https://
 URL.
EDGE3-05Presigned read for anonymous/authorized viewerThe image loads after a hard refresh; the actual <img>/network-request URL carries a fresh presigned query string (or redirect-service path) each time — not a stale expired signature.
EDGE3-06Legacy full-URL logo still renders (no backfill)An OrgProfile.logoUrl left as a pre-hardening full URL still resolves via presignStoredAsset's URL-parsing fallback — renders without needing a data migration.
EDGE3-07External avatar URL passed through untouched (#410)A member with avatarUrl set to an external host (e.g. https://robohash.org/
) renders that exact URL — it is not rewritten into an /images/members/
 or /secure-images/members/
 redirect path.

Not-in-scope

There is no admin UI page listing raw Upload audit rows — the sanitized originalFilename is backend-only. Verify it via the served Content-Disposition filename (a generated key, e.g. proof-<timestamp>-<random>.ext — NOT the sanitized display name) plus the absence of any 500/crash, not a direct UI read of the audit row.

➡ Full click-by-click steps: TC-EDGE-003 (Detailed).