security: add server-side validation for email status transitions

Implement strict validation to prevent unauthorized email status changes:
- Force status to Unverified when email address changes
- Only allow client to reset status to Unverified (via reset button)
- Block client from upgrading status (Unverified→Verified, etc.)
- All status upgrades must happen server-side via verification links

This prevents attackers from:
- Submitting new emails with fake "verified" status
- Bypassing email verification by modifying client requests
- Escalating email status without proper verification flow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-12-29 23:21:49 +01:00
parent fe98a63594
commit 4dc2df4a12

View File

@@ -176,6 +176,20 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
}; };
} }
// SECURITY: Validate email status transitions
// - If email changed: force to Unverified (prevents spoofing verified status)
// - If email unchanged: only allow client to reset to Unverified (via reset button)
// All other status transitions (Unverified→VerificationPending, VerificationPending→Verified)
// must happen server-side through other mechanisms (email verification links, etc.)
const emailHasChanged = currentLocation.tenantEmail !== (tenantEmail || null);
const clientWantsToReset = tenantEmailStatus === EmailStatus.Unverified;
const finalEmailStatus = emailHasChanged
? EmailStatus.Unverified // Email changed: force reset
: clientWantsToReset
? EmailStatus.Unverified // Client initiated reset: allow it
: (currentLocation.tenantEmailStatus || EmailStatus.Unverified); // Otherwise: keep current status
// Handle different update scopes // Handle different update scopes
if (updateScope === "current" || !updateScope) { if (updateScope === "current" || !updateScope) {
// Update only the current location (default behavior) // Update only the current location (default behavior)
@@ -194,7 +208,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
tenantTown: tenantTown || null, tenantTown: tenantTown || null,
autoBillFwd: autoBillFwd || false, autoBillFwd: autoBillFwd || false,
tenantEmail: tenantEmail || null, tenantEmail: tenantEmail || null,
tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified, tenantEmailStatus: finalEmailStatus,
billFwdStrategy: billFwdStrategy || "when-payed", billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotification: rentDueNotification || false, rentDueNotification: rentDueNotification || false,
rentDueDay: rentDueDay || null, rentDueDay: rentDueDay || null,
@@ -226,7 +240,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
tenantTown: tenantTown || null, tenantTown: tenantTown || null,
autoBillFwd: autoBillFwd || false, autoBillFwd: autoBillFwd || false,
tenantEmail: tenantEmail || null, tenantEmail: tenantEmail || null,
tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified, tenantEmailStatus: finalEmailStatus,
billFwdStrategy: billFwdStrategy || "when-payed", billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotification: rentDueNotification || false, rentDueNotification: rentDueNotification || false,
rentDueDay: rentDueDay || null, rentDueDay: rentDueDay || null,
@@ -251,7 +265,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
tenantTown: tenantTown || null, tenantTown: tenantTown || null,
autoBillFwd: autoBillFwd || false, autoBillFwd: autoBillFwd || false,
tenantEmail: tenantEmail || null, tenantEmail: tenantEmail || null,
tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified, tenantEmailStatus: finalEmailStatus,
billFwdStrategy: billFwdStrategy || "when-payed", billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotification: rentDueNotification || false, rentDueNotification: rentDueNotification || false,
rentDueDay: rentDueDay || null, rentDueDay: rentDueDay || null,