fix(portal): drop writable photoKey from PATCH /portal/pets — S3 key-hijack (GRO-2187/GRO-2198) #172
Reference in New Issue
Block a user
Delete Branch "fix/gro-2187-portal-photokey-hijack"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Security fix — drop writable
photoKeyfrom portal pet PATCH (S3 key-hijack)Closes the HIGH Security finding on the GRO-2187 portal pet PATCH (GRO-2198), ratified by the CTO.
The defect
PATCH /api/portal/pets/:petIdmappedbody.photoUrl → updateData.photoKeywith no validation.photoKeyis a trusted S3 object key consumed server-side bygetPresignedGetUrl(pet.photoKey)(read) anddeleteObject(pet.photoKey)(destructive). The upload path (src/routes/pets.ts:444) already guards keys with apets/{petId}/prefix "to prevent key hijacking" — the portal PATCH bypassed that guard, so an authenticated customer could point their pet'sphotoKeyat any object key → cross-tenant disclosure/destruction on a later staff action.The fix (per CTO directive — smallest safe surface)
photoUrlfromportalPetUpdateSchemaand delete theupdateData.photoKey = body.photoUrlline. Photo changes already have a dedicated, key-validated flow (upload +/photo/confirm). The web form's round-trip of the GET-shapedphotoUrlis a no-op — Zod strips the unknown key.preferredCuts(stringmax(2000), arraymax(50)),medicalAlerts(arraymax(50)), and medicalAlerttype/description(max(2000)), mirroring existing free-text caps.Tests (
src/__tests__/portalPets.test.ts)photoUrlon portal PATCH →200butphotoKeyunchanged.medicalAlert.description→400(zValidator).photoKeyis no longer written by the portal PATCH.Verification
npm run typecheck✅ ·npm run lint✅ (0 errors; only pre-existing warnings) ·npm test✅ 612 passed.Sequencing
dev → promote
dev→uat→ bump UAT GitOps overlay tag → redeploy. Then Security re-runs the gate and Shedward re-runs §4.8/§5.23. GRO-2198 stays blocked until the fixed image is live onuat; GRO-1480 stays open.Refs GRO-2187, GRO-2198. 🤖 Generated with Claude Code
Security gate FAIL (Barkley, CTO-ratified): the portal pet PATCH mapped `body.photoUrl -> updateData.photoKey` with no validation. photoKey is a trusted S3 object key consumed server-side by getPresignedGetUrl (read) and deleteObject (destructive); the upload path (pets.ts) already guards keys with a pets/{petId}/ prefix to prevent hijacking. The portal PATCH bypassed that guard, letting an authenticated customer point their pet's photoKey at any object key (cross-tenant disclosure/destruction on a later staff action). Fix (per CTO directive — smallest safe surface): - Remove `photoUrl` from portalPetUpdateSchema and delete the `updateData.photoKey = body.photoUrl` mapping. Photo changes already have a dedicated, key-validated flow (upload + /photo/confirm). The web form's round-trip of the GET-shaped photoUrl is a no-op (Zod strips the unknown key). - LOW hardening: cap preferredCuts (string max 2000, array max 50), medicalAlerts (array max 50) and medicalAlert type/description (max 2000), mirroring the existing free-text max(2000) caps. Tests: - New regression: foreign photoUrl on portal PATCH returns 200 but leaves photoKey unchanged (gate evidence). - New: over-long medicalAlert description rejected (400, zValidator). - Updated the existing "persists mapped columns" test: photoKey is no longer written by the portal PATCH. Co-Authored-By: Paperclip <noreply@paperclip.ing>