fix: harden auth, cron secrets, and provider flows (GNU-17) (#148)
- Replace === with crypto.timingSafeEqual in all 7 cron routes via shared lib/auth/cron.ts - Add in-memory rate limiting (60 req/min) and expires_at support to calendar feed - Add exponential backoff on MFA verify after 3 failed attempts - Add 60s cooldown on password reset requests - Validate bank callback auth code format before API call - Redact session IDs from bank sync and callback logs - Validate OAuth redirect_uris against allowlist (claude.ai, claude.com, localhost) - Remove excessive PII/debug console logging from login page Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5c8db1ec60
commit
550cadcb06
@@ -2,6 +2,22 @@ import { createClient } from '@supabase/supabase-js'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { generateCalendarFeed } from '@/lib/calendar/ics-generator'
|
||||
|
||||
// In-memory rate limiting: token -> { count, resetAt }
|
||||
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
||||
const RATE_LIMIT_WINDOW_MS = 60_000 // 1 minute
|
||||
const RATE_LIMIT_MAX = 60 // 60 requests per minute per token
|
||||
|
||||
// Periodic cleanup to prevent memory leaks (every 5 minutes)
|
||||
let lastCleanup = Date.now()
|
||||
function cleanupRateLimitMap() {
|
||||
const now = Date.now()
|
||||
if (now - lastCleanup < 5 * 60_000) return
|
||||
lastCleanup = now
|
||||
for (const [key, value] of rateLimitMap) {
|
||||
if (now > value.resetAt) rateLimitMap.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/calendar/feed/[token]
|
||||
* Returns an ICS calendar feed for the given token
|
||||
@@ -19,6 +35,19 @@ export async function GET(
|
||||
return new NextResponse('Invalid token', { status: 400 })
|
||||
}
|
||||
|
||||
// Rate limiting per token
|
||||
cleanupRateLimitMap()
|
||||
const nowMs = Date.now()
|
||||
const rateEntry = rateLimitMap.get(token)
|
||||
if (rateEntry && nowMs < rateEntry.resetAt) {
|
||||
if (rateEntry.count >= RATE_LIMIT_MAX) {
|
||||
return new NextResponse('Too many requests', { status: 429 })
|
||||
}
|
||||
rateEntry.count++
|
||||
} else {
|
||||
rateLimitMap.set(token, { count: 1, resetAt: nowMs + RATE_LIMIT_WINDOW_MS })
|
||||
}
|
||||
|
||||
// Create service client (no user auth required)
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
||||
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
@@ -41,6 +70,11 @@ export async function GET(
|
||||
return new NextResponse('Feed not found or inactive', { status: 404 })
|
||||
}
|
||||
|
||||
// Check token expiry
|
||||
if (feed.expires_at && new Date(feed.expires_at) < new Date()) {
|
||||
return new NextResponse('Feed token has expired', { status: 410 })
|
||||
}
|
||||
|
||||
// Update access tracking
|
||||
await supabase
|
||||
.from('calendar_feeds')
|
||||
|
||||
Reference in New Issue
Block a user