mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
0a9bd39aac
ref OUT-Q325-03
108 lines
3.2 KiB
TypeScript
108 lines
3.2 KiB
TypeScript
import type { Next } from "koa";
|
|
import { Scope } from "@shared/types";
|
|
import env from "@server/env";
|
|
import AuthenticationHelper from "@shared/helpers/AuthenticationHelper";
|
|
import { AppContext } from "@server/types";
|
|
import {
|
|
generateRawToken,
|
|
bundleToken,
|
|
unbundleToken,
|
|
} from "@server/utils/csrf";
|
|
import { getCookieDomain } from "@shared/utils/domains";
|
|
import { CSRF } from "@shared/constants";
|
|
import { CSRFError } from "@server/errors";
|
|
|
|
/**
|
|
* Middleware that generates and attaches CSRF tokens for safe methods
|
|
*/
|
|
export function attachCSRFToken() {
|
|
return async function attachCSRFTokenMiddleware(ctx: AppContext, next: Next) {
|
|
// Only attach tokens for safe methods that don't mutate state
|
|
if (["GET", "HEAD", "OPTIONS"].includes(ctx.method)) {
|
|
const raw = generateRawToken(16);
|
|
const bundled = bundleToken(raw, env.SECRET_KEY);
|
|
|
|
// Set cookie that JavaScript can read (not HttpOnly)
|
|
ctx.cookies.set(CSRF.cookieName, bundled, {
|
|
httpOnly: false,
|
|
sameSite: "lax",
|
|
domain: getCookieDomain(ctx.request.hostname, env.isCloudHosted),
|
|
});
|
|
}
|
|
|
|
await next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Middleware that verifies CSRF tokens for mutating requests
|
|
*/
|
|
export function verifyCSRFToken() {
|
|
/**
|
|
* Determines if a request requires CSRF protection
|
|
*/
|
|
const shouldProtectRequest = (ctx: AppContext): boolean => {
|
|
// Skip if not a potentially mutating method
|
|
if (["GET", "HEAD", "OPTIONS"].includes(ctx.method)) {
|
|
return false;
|
|
}
|
|
|
|
// If not using cookie-based auth, skip CSRF protection
|
|
if (!ctx.cookies.get("accessToken")) {
|
|
return false;
|
|
}
|
|
|
|
// For API routes, use AuthenticationHelper to determine if the operation is read-only
|
|
if (ctx.originalUrl.startsWith("/api/")) {
|
|
const canAccessWithReadOnly = AuthenticationHelper.canAccess(ctx.path, [
|
|
Scope.Read,
|
|
]);
|
|
|
|
// If it can be accessed with read-only scope, it doesn't need CSRF protection
|
|
if (canAccessWithReadOnly) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Protect all other mutating requests
|
|
return true;
|
|
};
|
|
|
|
return async function verifyCSRFTokenMiddleware(ctx: AppContext, next: Next) {
|
|
if (!shouldProtectRequest(ctx)) {
|
|
await next();
|
|
return;
|
|
}
|
|
|
|
// Get token from cookie
|
|
const cookieVal = ctx.cookies.get(CSRF.cookieName);
|
|
if (!cookieVal) {
|
|
throw CSRFError("CSRF token missing from cookie");
|
|
}
|
|
|
|
// Get token from header or form field depending on type
|
|
// Access the already-parsed body from koa-body middleware
|
|
const inputVal =
|
|
ctx.get(CSRF.headerName) || ctx.request.body?.[CSRF.fieldName];
|
|
|
|
if (!inputVal) {
|
|
throw CSRFError("CSRF token missing from request");
|
|
}
|
|
|
|
// Verify both tokens are valid HMAC-signed tokens
|
|
const { valid: cookieValid } = unbundleToken(cookieVal, env.SECRET_KEY);
|
|
const { valid: inputValid } = unbundleToken(inputVal, env.SECRET_KEY);
|
|
|
|
if (!cookieValid || !inputValid) {
|
|
throw CSRFError("CSRF token invalid or malformed");
|
|
}
|
|
|
|
// Verify tokens match (double-submit check)
|
|
if (cookieVal !== inputVal) {
|
|
throw CSRFError("CSRF token mismatch");
|
|
}
|
|
|
|
await next();
|
|
};
|
|
}
|