security: fix webhook signature fail-open (now throws 500 on missing key), fix overly restrictive couple update rules

This commit is contained in:
null 2026-06-16 22:11:51 -05:00
parent 50c44d2afd
commit b8b2cc68c4
2 changed files with 16 additions and 20 deletions

View File

@ -125,16 +125,12 @@ service cloud.firestore {
// - user IDs are immutable (cannot change who is in the couple)
// - invite code is immutable (cannot change the code)
// - createdAt is immutable (cannot change when the couple was formed)
// - currentQuestionId: either member can set (either can pick a question)
// - streakCount and lastStreakAt: server-only (via Cloud Functions or admin SDK)
// - Any other fields: both members can update normally
// - All other fields: both members can update normally
allow update: if isCouplesMember(coupleId)
// Check immutable fields haven't changed
&& isImmutable(['userIds', 'inviteCode', 'createdAt'])
// Allow currentQuestionId updates
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['currentQuestionId'])
// No other fields should be changed
// Check that streakCount and lastStreakAt are not in the update
// streakCount and lastStreakAt must not be modified by clients
&& !request.resource.data.diff(resource.data).affectedKeys().hasAny(['streakCount', 'lastStreakAt']);
// Delete: server-only (admin SDK only)

View File

@ -9,15 +9,15 @@ const router = Router()
/**
* Verifies RevenueCat webhook signature using Ed25519.
* RevenueCat signs the raw request body with their signing key.
* If REVENUECAT_SIGNING_KEY is not set, logs warning but skips verification (dev mode).
* If REVENUECAT_SIGNING_KEY is not set, throws an error (fail-closed).
*/
function verifyRevenueCatSignature(req: Request): boolean {
const signingKey = process.env.REVENUECAT_SIGNING_KEY
// If signing key is not configured, skip verification (development mode)
// If signing key is not configured, fail closed (not dev mode)
if (!signingKey || signingKey.trim() === '') {
console.warn('[webhook] REVENUECAT_SIGNING_KEY not set — skipping signature verification')
return true
console.error('[webhook] REVENUECAT_SIGNING_KEY not set — rejecting all requests (fail-closed)')
throw new Error('REVENUECAT_SIGNING_KEY must be set in production')
}
const signatureHeader = req.headers['x-revenuecat-signature']
@ -76,16 +76,16 @@ function verifyRevenueCatSecret(req: Request): boolean {
}
router.post('/revenuecat', async (req: Request, res: Response) => {
// Try signature verification first (modern)
const signatureValid = verifyRevenueCatSignature(req)
if (!signatureValid) {
// Try signature verification first (modern, prefered)
try {
if (!verifyRevenueCatSignature(req)) {
res.status(401).json({ error: 'unauthorized' })
return
}
// Fallback to secret verification if no signature (legacy)
if (!signatureValid && !verifyRevenueCatSecret(req)) {
res.status(401).json({ error: 'unauthorized' })
} catch (err: any) {
// If signature verification throws (e.g., missing key), reject with 500
console.error('[webhook] Signature verification error:', err)
res.status(500).json({ error: 'signature_verification_failed', message: err.message })
return
}