If a cookie is signed for one purpose, but the server also uses a differently named signed cookie name for another purpose, then it's possible for a malicious client to substitute the value of one signed cookie with the value of another and have it pass the signature check. Include the name of the cookie when computing the signature so that no cookies signed for example for "user_session" can be used as a value for a hypothetical "admin_session" cookie.
42 lines
1.3 KiB
TypeScript
42 lines
1.3 KiB
TypeScript
import type { H3Event } from "h3";
|
|
|
|
let cachedCookieSecret: CryptoKey;
|
|
export async function useCookieSecret(event: H3Event) {
|
|
if (cachedCookieSecret)
|
|
return cachedCookieSecret;
|
|
|
|
const runtimeConfig = useRuntimeConfig(event);
|
|
return cachedCookieSecret = await crypto.subtle.importKey(
|
|
"raw",
|
|
Buffer.from(runtimeConfig.cookieSecretKey, "base64url"),
|
|
{ name: "HMAC", hash: "SHA-256" },
|
|
false,
|
|
["sign", "verify"],
|
|
);
|
|
}
|
|
|
|
export async function setSignedCookie(event: H3Event, name: string, value: string, maxAge?: number) {
|
|
const secret = await useCookieSecret(event);
|
|
const signature = await crypto.subtle.sign("HMAC", secret, Buffer.from(`${name}=${value}`));
|
|
const cookie = `${value}.${Buffer.from(signature).toString("base64url")}`
|
|
setCookie(event, name, cookie, { httpOnly: true, secure: true, sameSite: true, maxAge });
|
|
}
|
|
|
|
export async function getSignedCookie(event: H3Event, name: string) {
|
|
const cookie = getCookie(event, name);
|
|
if (!cookie)
|
|
return;
|
|
|
|
const rightDot = cookie.lastIndexOf(".");
|
|
if (rightDot === -1)
|
|
return;
|
|
|
|
const value = cookie.slice(0, rightDot);
|
|
const secret = await useCookieSecret(event);
|
|
const signature = Buffer.from(cookie.slice(rightDot + 1), "base64url");
|
|
const valid = await crypto.subtle.verify("HMAC", secret, signature, Buffer.from(`${name}=${value}`));
|
|
if (!valid)
|
|
return
|
|
|
|
return value;
|
|
}
|