Round 19: Fix auth 500/session persistence, script tag warning, module factory error, rate limiting
- Fix auth session persistence: set cookie on both cookies() and NextResponse - Fix isSecureRequest: check x-forwarded-proto for HTTPS proxy support - Fix excessive GET /api/auth polling: add useRef guard to prevent duplicate checks - Increase rate limits: auth POST 30/min, auth GET 120/min - Fix script tag warning: use ref-based textContent instead of dangerouslySetInnerHTML in chart.tsx - Fix sanitizeHtml: improve regex patterns for script tag removal - Fix module factory error: disable SW registration in dev, skip /_next/ caching in sw.js - Bump SW cache version v2→v3 to force cache invalidation - Better error handling in auth route with specific error messages
This commit is contained in:
1 parent
760fa70e82
commit
74404708e5
9 files changed
+192
-47
No files matched your search
+30
-10
@@ -3,8 +3,33 @@ import { cookies } from 'next/headers';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
const SESSION_COOKIE_NAME = 'ct_session';
|
||||
const SESSION_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
|
||||
export const SESSION_COOKIE_NAME = 'ct_session';
|
||||
export const SESSION_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
|
||||
|
||||
/**
|
||||
* Determine whether the Secure flag should be set on cookies.
|
||||
* Returns true when:
|
||||
* - NODE_ENV is production, OR
|
||||
* - the request came through an HTTPS proxy (x-forwarded-proto: https)
|
||||
*/
|
||||
export function isSecureRequest(request: Request): boolean {
|
||||
if (process.env.NODE_ENV === 'production') return true;
|
||||
const forwarded = request.headers.get('x-forwarded-proto');
|
||||
return forwarded === 'https';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cookie options object for the session cookie.
|
||||
*/
|
||||
export function getSessionCookieOptions(secure: boolean) {
|
||||
return {
|
||||
httpOnly: true,
|
||||
secure,
|
||||
sameSite: 'lax' as const,
|
||||
maxAge: SESSION_COOKIE_MAX_AGE,
|
||||
path: '/',
|
||||
};
|
||||
}
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
@@ -18,15 +43,10 @@ export async function verifyPassword(
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
export async function createSession(userId: string): Promise<void> {
|
||||
export async function createSession(userId: string, secure?: boolean): Promise<void> {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(SESSION_COOKIE_NAME, userId, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: SESSION_COOKIE_MAX_AGE,
|
||||
path: '/',
|
||||
});
|
||||
const cookieSecure = secure ?? (process.env.NODE_ENV === 'production');
|
||||
cookieStore.set(SESSION_COOKIE_NAME, userId, getSessionCookieOptions(cookieSecure));
|
||||
}
|
||||
|
||||
const sessionUserSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||
|
||||
@@ -23,8 +23,11 @@ setInterval(() => {
|
||||
// ─── Preset Limits ────────────────────────────────────────────────────
|
||||
|
||||
export const RATE_LIMITS = {
|
||||
// Auth endpoints: 20 requests per minute (allows retries & multiple tabs)
|
||||
auth: { limit: 20, windowMs: 60 * 1000 },
|
||||
// Auth POST endpoints: 30 requests per minute (allows retries & multiple tabs)
|
||||
auth: { limit: 30, windowMs: 60 * 1000 },
|
||||
|
||||
// Auth GET endpoint: 120 requests per minute (high frequency, low cost)
|
||||
authGet: { limit: 120, windowMs: 60 * 1000 },
|
||||
|
||||
// Data read endpoints: 60 requests per minute (generous)
|
||||
dataRead: { limit: 60, windowMs: 60 * 1000 },
|
||||
|
||||
+15
-5
@@ -13,15 +13,25 @@ export function cn(...inputs: ClassValue[]) {
|
||||
export function sanitizeHtml(html: string): string {
|
||||
if (!html) return '';
|
||||
return html
|
||||
// Remove <script> tags and their content
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
// Remove <script> tags and their content (handles multi-line, attributes, and unclosed tags)
|
||||
.replace(/<script\b[\s\S]*?<\/script\s*>/gi, '')
|
||||
// Remove unclosed <script> tags (no closing tag)
|
||||
.replace(/<script\b[^>]*>[\s\S]*$/gi, '')
|
||||
// Remove self-closing <script /> tags
|
||||
.replace(/<script\b[^>]*\/>/gi, '')
|
||||
// Remove on* event handler attributes (onclick, onload, onerror, etc.)
|
||||
.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
|
||||
// Remove javascript: URLs
|
||||
.replace(/href\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, 'href="#"')
|
||||
// Remove <iframe> tags
|
||||
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
|
||||
.replace(/<iframe\b[\s\S]*?<\/iframe\s*>/gi, '')
|
||||
// Remove <object> and <embed> tags
|
||||
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, '')
|
||||
.replace(/<embed\b[^>]*>/gi, '');
|
||||
.replace(/<object\b[\s\S]*?<\/object\s*>/gi, '')
|
||||
.replace(/<embed\b[^>]*>/gi, '')
|
||||
// Remove <noscript> tags (may contain script-like content)
|
||||
.replace(/<noscript\b[\s\S]*?<\/noscript\s*>/gi, '')
|
||||
// Remove <link> tags (could load external scripts)
|
||||
.replace(/<link\b[^>]*>/gi, '')
|
||||
// Remove <meta> tags with http-equiv="refresh" (could redirect)
|
||||
.replace(/<meta\b[^>]*http-equiv\s*=\s*["']?refresh["']?[^>]*>/gi, '');
|
||||
}
|
||||
Reference in new issue
Block a user