v1.0.4: Security hardening - rate limiting, middleware, HSTS, password strength, anti-enumeration
This commit is contained in:
@@ -2,9 +2,16 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/db'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { rateLimit, getClientIp, rateLimitResponse } from '@/lib/rate-limit'
|
||||
|
||||
const changePwLimiter = rateLimit({ id: 'change-pw', max: 5, windowSeconds: 60 * 15 })
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const ip = getClientIp(req)
|
||||
const rl = changePwLimiter.check(ip)
|
||||
if (!rl.success) return rateLimitResponse(rl.resetAt)
|
||||
|
||||
const user = await getSession()
|
||||
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
|
||||
@@ -14,8 +21,8 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'Beide Felder sind erforderlich' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return NextResponse.json({ error: 'Neues Kennwort muss mindestens 6 Zeichen lang sein' }, { status: 400 })
|
||||
if (newPassword.length < 8) {
|
||||
return NextResponse.json({ error: 'Neues Kennwort muss mindestens 8 Zeichen lang sein' }, { status: 400 })
|
||||
}
|
||||
|
||||
const dbUser = await (prisma as any).user.findUnique({
|
||||
|
||||
@@ -3,10 +3,15 @@ import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { deleteAccountLimiter, getClientIp, rateLimitResponse } from '@/lib/rate-limit'
|
||||
|
||||
// POST: User deletes their own account
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const ip = getClientIp(req)
|
||||
const rl = deleteAccountLimiter.check(ip)
|
||||
if (!rl.success) return rateLimitResponse(rl.resetAt)
|
||||
|
||||
const session = await getSession()
|
||||
if (!session) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
||||
|
||||
|
||||
@@ -2,9 +2,14 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { sendEmail, getSmtpConfig } from '@/lib/email'
|
||||
import { forgotPasswordLimiter, getClientIp, rateLimitResponse } from '@/lib/rate-limit'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const ip = getClientIp(req)
|
||||
const rl = forgotPasswordLimiter.check(ip)
|
||||
if (!rl.success) return rateLimitResponse(rl.resetAt)
|
||||
|
||||
const { email } = await req.json()
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: 'E-Mail erforderlich' }, { status: 400 })
|
||||
|
||||
@@ -3,9 +3,14 @@ import { cookies } from 'next/headers'
|
||||
import { login, createToken } from '@/lib/auth'
|
||||
import { loginSchema } from '@/lib/validations'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { loginLimiter, getClientIp, rateLimitResponse } from '@/lib/rate-limit'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const ip = getClientIp(request)
|
||||
const rl = loginLimiter.check(ip)
|
||||
if (!rl.success) return rateLimitResponse(rl.resetAt)
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
const validated = loginSchema.safeParse(body)
|
||||
|
||||
@@ -4,16 +4,21 @@ import { hashPassword } from '@/lib/auth'
|
||||
import { sendEmail } from '@/lib/email'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { z } from 'zod'
|
||||
import { registerLimiter, getClientIp, rateLimitResponse } from '@/lib/rate-limit'
|
||||
|
||||
const registerSchema = z.object({
|
||||
organizationName: z.string().min(2, 'Organisationsname zu kurz').max(200),
|
||||
name: z.string().min(2, 'Name zu kurz').max(200),
|
||||
email: z.string().email('Ungültige E-Mail-Adresse'),
|
||||
password: z.string().min(6, 'Passwort muss mindestens 6 Zeichen haben'),
|
||||
password: z.string().min(8, 'Passwort muss mindestens 8 Zeichen haben'),
|
||||
})
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const ip = getClientIp(req)
|
||||
const rl = registerLimiter.check(ip)
|
||||
if (!rl.success) return rateLimitResponse(rl.resetAt)
|
||||
|
||||
const body = await req.json()
|
||||
const data = registerSchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,9 +2,14 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { sendEmail } from '@/lib/email'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { resendVerificationLimiter, getClientIp, rateLimitResponse } from '@/lib/rate-limit'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const ip = getClientIp(req)
|
||||
const rl = resendVerificationLimiter.check(ip)
|
||||
if (!rl.success) return rateLimitResponse(rl.resetAt)
|
||||
|
||||
const { email } = await req.json()
|
||||
|
||||
if (!email) {
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { hashPassword } from '@/lib/auth'
|
||||
import { resetPasswordLimiter, getClientIp, rateLimitResponse } from '@/lib/rate-limit'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const ip = getClientIp(req)
|
||||
const rl = resetPasswordLimiter.check(ip)
|
||||
if (!rl.success) return rateLimitResponse(rl.resetAt)
|
||||
|
||||
const { token, password } = await req.json()
|
||||
if (!token || !password) {
|
||||
return NextResponse.json({ error: 'Token und Passwort erforderlich' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return NextResponse.json({ error: 'Passwort muss mindestens 6 Zeichen lang sein' }, { status: 400 })
|
||||
if (password.length < 8) {
|
||||
return NextResponse.json({ error: 'Passwort muss mindestens 8 Zeichen lang sein' }, { status: 400 })
|
||||
}
|
||||
|
||||
const user = await (prisma as any).user.findFirst({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { sendEmail, getSmtpConfig } from '@/lib/email'
|
||||
import { z } from 'zod'
|
||||
import { contactLimiter, getClientIp, rateLimitResponse } from '@/lib/rate-limit'
|
||||
|
||||
const contactSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
@@ -23,6 +24,10 @@ async function getContactEmail(): Promise<string> {
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const ip = getClientIp(req)
|
||||
const rl = contactLimiter.check(ip)
|
||||
if (!rl.success) return rateLimitResponse(rl.resetAt)
|
||||
|
||||
const body = await req.json()
|
||||
const data = contactSchema.parse(body)
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ export default function RegisterPage() {
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
toast({ title: 'Passwort muss mindestens 6 Zeichen haben', variant: 'destructive' })
|
||||
if (password.length < 8) {
|
||||
toast({ title: 'Passwort muss mindestens 8 Zeichen haben', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function RegisterPage() {
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Mindestens 6 Zeichen"
|
||||
placeholder="Mindestens 8 Zeichen"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
|
||||
@@ -32,8 +32,8 @@ function ResetPasswordForm() {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
|
||||
if (password.length < 6) {
|
||||
setError('Passwort muss mindestens 6 Zeichen lang sein.')
|
||||
if (password.length < 8) {
|
||||
setError('Passwort muss mindestens 8 Zeichen lang sein.')
|
||||
return
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
@@ -108,7 +108,7 @@ function ResetPasswordForm() {
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Min. 6 Zeichen"
|
||||
placeholder="Min. 8 Zeichen"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
|
||||
Reference in New Issue
Block a user