diff --git a/docs/pages/guides/email-and-password/basics.md b/docs/pages/guides/email-and-password/basics.md index 7f3bada46..0631a2734 100644 --- a/docs/pages/guides/email-and-password/basics.md +++ b/docs/pages/guides/email-and-password/basics.md @@ -148,7 +148,16 @@ app.post("/login", async (request: Request) => { const user = await db.table("user").where("email", "=", email).get(); if (!user) { - // invalid email + // NOTE: + // Returning immediately allows malicious actors to figure out valid emails from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid emails. + // However, valid emails can be already be revealed with the signup page + // and a similar timing issue can likely be found in password reset implementation. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If emails/usernames are public, you may outright tell the user that the username is invalid. return new Response("Invalid email or password", { status: 400 }); diff --git a/docs/pages/guides/validate-session-cookies/astro.md b/docs/pages/guides/validate-session-cookies/astro.md index b361b57df..967da98a2 100644 --- a/docs/pages/guides/validate-session-cookies/astro.md +++ b/docs/pages/guides/validate-session-cookies/astro.md @@ -17,7 +17,8 @@ import { defineMiddleware } from "astro:middleware"; export const onRequest = defineMiddleware(async (context, next) => { if (context.request.method !== "GET") { const originHeader = request.headers.get("Origin"); - const hostHeader = request.headers.get("Header"); + // NOTE: You may need to use `X-Forwarded-Host` instead + const hostHeader = request.headers.get("Host"); if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { return new Response(null, { status: 403 diff --git a/docs/pages/guides/validate-session-cookies/elysia.md b/docs/pages/guides/validate-session-cookies/elysia.md index 39f89ed60..83aa12567 100644 --- a/docs/pages/guides/validate-session-cookies/elysia.md +++ b/docs/pages/guides/validate-session-cookies/elysia.md @@ -24,6 +24,7 @@ const app = new Elysia().derive( // CSRF check if (context.request.method !== "GET") { const originHeader = context.request.headers.get("Origin"); + // NOTE: You may need to use `X-Forwarded-Host` instead const hostHeader = context.request.headers.get("Host"); if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { return { diff --git a/docs/pages/guides/validate-session-cookies/express.md b/docs/pages/guides/validate-session-cookies/express.md index 0f2c2fbef..85b736942 100644 --- a/docs/pages/guides/validate-session-cookies/express.md +++ b/docs/pages/guides/validate-session-cookies/express.md @@ -20,6 +20,7 @@ app.use((req, res, next) => { return next(); } const originHeader = req.headers.origin ?? null; + // NOTE: You may need to use `X-Forwarded-Host` instead const hostHeader = req.headers.host ?? null; if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { return res.status(403).end(); diff --git a/docs/pages/guides/validate-session-cookies/hono.md b/docs/pages/guides/validate-session-cookies/hono.md index 64f83680a..809103ae4 100644 --- a/docs/pages/guides/validate-session-cookies/hono.md +++ b/docs/pages/guides/validate-session-cookies/hono.md @@ -28,6 +28,7 @@ app.use("*", (c, next) => { return next(); } const originHeader = c.req.headers.get("Origin"); + // NOTE: You may need to use `X-Forwarded-Host` instead const hostHeader = c.req.headers.get("Host"); if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { return c.body(null, 403); diff --git a/docs/pages/guides/validate-session-cookies/index.md b/docs/pages/guides/validate-session-cookies/index.md index 540f7a01c..6e2ce8202 100644 --- a/docs/pages/guides/validate-session-cookies/index.md +++ b/docs/pages/guides/validate-session-cookies/index.md @@ -16,15 +16,16 @@ This guide is also available for: - [SolidStart](/guides/validate-session-cookies/solidstart) - [SvelteKit](/guides/validate-session-cookies/sveltekit) -**CSRF protection must be implemented when using cookies and forms.** This can be easily done by comparing the `Origin` and `Host` header. +**CSRF protection must be implemented when using cookies and forms.** This can be easily done by comparing the `Origin` and `Host` header. For non-GET requests, check the request origin. You can use `readSessionCookie()` to get the session cookie from a HTTP `Cookie` header, and validate it with `Lucia.validateSession()`. Make sure to delete the session cookie if it's invalid and create a new session cookie when the expiration gets extended, which is indicated by `Session.fresh`. ```ts import { verifyRequestOrigin } from "lucia"; -// only required in non-GET requests (POST, PUT, DELETE, PATCH, etc) +// Only required in non-GET requests (POST, PUT, DELETE, PATCH, etc) const originHeader = request.headers.get("Origin"); +// NOTE: You may need to use `X-Forwarded-Host` instead const hostHeader = request.headers.get("Host"); if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { return new Response(null, { diff --git a/docs/pages/guides/validate-session-cookies/nextjs-app.md b/docs/pages/guides/validate-session-cookies/nextjs-app.md index 351d944a6..472678fd5 100644 --- a/docs/pages/guides/validate-session-cookies/nextjs-app.md +++ b/docs/pages/guides/validate-session-cookies/nextjs-app.md @@ -87,6 +87,7 @@ export async function middleware(request: NextRequest): Promise { return NextResponse.next(); } const originHeader = request.headers.get("Origin"); + // NOTE: You may need to use `X-Forwarded-Host` instead const hostHeader = request.headers.get("Host"); if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { return new NextResponse(null, { diff --git a/docs/pages/guides/validate-session-cookies/nextjs-pages.md b/docs/pages/guides/validate-session-cookies/nextjs-pages.md index bef44cb9c..5d5af47b4 100644 --- a/docs/pages/guides/validate-session-cookies/nextjs-pages.md +++ b/docs/pages/guides/validate-session-cookies/nextjs-pages.md @@ -17,6 +17,7 @@ export async function middleware(request: NextRequest): Promise { return NextResponse.next(); } const originHeader = request.headers.get("Origin"); + // NOTE: You may need to use `X-Forwarded-Host` instead const hostHeader = request.headers.get("Host"); if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { return new NextResponse(null, { diff --git a/docs/pages/guides/validate-session-cookies/nuxt.md b/docs/pages/guides/validate-session-cookies/nuxt.md index 173a26129..9547bd803 100644 --- a/docs/pages/guides/validate-session-cookies/nuxt.md +++ b/docs/pages/guides/validate-session-cookies/nuxt.md @@ -17,6 +17,7 @@ import type { Session, User } from "lucia"; export default defineEventHandler(async (event) => { if (event.method !== "GET") { const originHeader = getHeader(event, "Origin") ?? null; + // NOTE: You may need to use `X-Forwarded-Host` instead const hostHeader = getHeader(event, "Host") ?? null; if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { return event.node.res.writeHead(403).end(); diff --git a/docs/pages/guides/validate-session-cookies/solidstart.md b/docs/pages/guides/validate-session-cookies/solidstart.md index 1ae6d5d14..f59be02a7 100644 --- a/docs/pages/guides/validate-session-cookies/solidstart.md +++ b/docs/pages/guides/validate-session-cookies/solidstart.md @@ -17,6 +17,7 @@ import { lucia } from "./lib/auth"; export default defineEventHandler((event) => { if (context.request.method !== "GET") { const originHeader = getHeader(event, "Origin") ?? null; + // NOTE: You may need to use `X-Forwarded-Host` instead const hostHeader = getHeader(event, "Host") ?? null; if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { return event.node.res.writeHead(403).end(); diff --git a/docs/pages/tutorials/username-and-password/astro.md b/docs/pages/tutorials/username-and-password/astro.md index 07d11090d..06de5fb89 100644 --- a/docs/pages/tutorials/username-and-password/astro.md +++ b/docs/pages/tutorials/username-and-password/astro.md @@ -190,6 +190,15 @@ export async function POST(context: APIContext): Promise { .where("username", "=", username.toLowerCase()) .get(); if (!existingUser) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid usernames from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid usernames. + // However, valid usernames can be already be revealed with the signup page among other methods. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If usernames are public, you may outright tell the user that the username is invalid. return new Response("Incorrect username or password", { status: 400 }); diff --git a/docs/pages/tutorials/username-and-password/nextjs-app.md b/docs/pages/tutorials/username-and-password/nextjs-app.md index 5964e557b..fc4c9239d 100644 --- a/docs/pages/tutorials/username-and-password/nextjs-app.md +++ b/docs/pages/tutorials/username-and-password/nextjs-app.md @@ -212,6 +212,15 @@ async function login(_: any, formData: FormData): Promise { .where("username", "=", username.toLowerCase()) .get(); if (!existingUser) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid usernames from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid usernames. + // However, valid usernames can be already be revealed with the signup page among other methods. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If usernames are public, you may outright tell the user that the username is invalid. return { error: "Incorrect username or password" }; diff --git a/docs/pages/tutorials/username-and-password/nextjs-pages.md b/docs/pages/tutorials/username-and-password/nextjs-pages.md index c488b98fd..dc0db6088 100644 --- a/docs/pages/tutorials/username-and-password/nextjs-pages.md +++ b/docs/pages/tutorials/username-and-password/nextjs-pages.md @@ -240,6 +240,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) .where("username", "=", username.toLowerCase()) .get(); if (!existingUser) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid usernames from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid usernames. + // However, valid usernames can be already be revealed with the signup page among other methods. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If usernames are public, you may outright tell the user that the username is invalid. res.status(400).json({ error: "Incorrect username or password" }); diff --git a/docs/pages/tutorials/username-and-password/nuxt.md b/docs/pages/tutorials/username-and-password/nuxt.md index 4618ea4d8..b93e63fbe 100644 --- a/docs/pages/tutorials/username-and-password/nuxt.md +++ b/docs/pages/tutorials/username-and-password/nuxt.md @@ -209,6 +209,15 @@ export default eventHandler(async (event) => { .where("username", "=", username.toLowerCase()) .get(); if (!existingUser) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid usernames from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid usernames. + // However, valid usernames can be already be revealed with the signup page among other methods. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If usernames are public, you may outright tell the user that the username is invalid. throw createError({ message: "Incorrect username or password", statusCode: 400 diff --git a/docs/pages/tutorials/username-and-password/sveltekit.md b/docs/pages/tutorials/username-and-password/sveltekit.md index 64ac9ae40..278bc8d55 100644 --- a/docs/pages/tutorials/username-and-password/sveltekit.md +++ b/docs/pages/tutorials/username-and-password/sveltekit.md @@ -123,7 +123,7 @@ export const actions: Actions = { ...sessionCookie.attributes }); - redirect(302, "/"); + redirect(302, "/"); } }; ``` @@ -198,6 +198,15 @@ export const actions: Actions = { .where("username", "=", username.toLowerCase()) .get(); if (!existingUser) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid usernames from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid usernames. + // However, valid usernames can be already be revealed with the signup page among other methods. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If usernames are public, you may outright tell the user that the username is invalid. return fail(400, { message: "Incorrect username or password" }); @@ -217,7 +226,7 @@ export const actions: Actions = { ...sessionCookie.attributes }); - redirect(302, "/"); + redirect(302, "/"); } }; ``` @@ -231,7 +240,7 @@ You can validate requests by checking `locals.user`. The field `user.username` i import type { PageServerLoad, Actions } from "./$types"; export const load: PageServerLoad = async (event) => { - if (!event.locals.user) redirect(302, "/login"); + if (!event.locals.user) redirect(302, "/login"); return { username: event.locals.user.username }; @@ -264,7 +273,7 @@ export const actions: Actions = { path: ".", ...sessionCookie.attributes }); - redirect(302, "/login"); + redirect(302, "/login"); } }; ```