v1.0.4: Security hardening - rate limiting, middleware, HSTS, password strength, anti-enumeration

This commit is contained in:
Pepe Ziberi
2026-02-21 18:55:10 +01:00
parent b75bf9bb30
commit 8ef2cbe68e
15 changed files with 289 additions and 14 deletions

View File

@@ -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({

View File

@@ -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 })

View File

@@ -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 })

View File

@@ -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)

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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({

View File

@@ -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)