diff --git a/api/src/unraid-api/auth/auth.service.ts b/api/src/unraid-api/auth/auth.service.ts index 7744ba66e..dbf114903 100644 --- a/api/src/unraid-api/auth/auth.service.ts +++ b/api/src/unraid-api/auth/auth.service.ts @@ -4,6 +4,7 @@ import { AuthZService } from 'nest-authz'; import type { UserAccount } from '@app/graphql/generated/api/types'; import { Role } from '@app/graphql/generated/api/types'; +import { getters } from '@app/store'; import { handleAuthError } from '@app/utils'; import { ApiKeyService } from './api-key.service'; @@ -205,6 +206,10 @@ export class AuthService { } } + public validateCsrfToken(token?: string): boolean { + return Boolean(token) && token === getters.emhttp().var.csrfToken; + } + /** * Returns a user object representing a session. * Note: Does NOT perform validation. diff --git a/api/src/unraid-api/auth/cookie.strategy.ts b/api/src/unraid-api/auth/cookie.strategy.ts index 598b23470..8383116d2 100644 --- a/api/src/unraid-api/auth/cookie.strategy.ts +++ b/api/src/unraid-api/auth/cookie.strategy.ts @@ -18,6 +18,9 @@ export class UserCookieStrategy extends PassportStrategy(Strategy, strategyName) } public validate = async (req: CustomRequest): Promise => { - return this.authService.validateCookiesCasbin(req.cookies); + return ( + this.authService.validateCsrfToken(req.headers['x-csrf-token']) && + this.authService.validateCookiesCasbin(req.cookies) + ); }; } diff --git a/api/src/unraid-api/types/request.ts b/api/src/unraid-api/types/request.ts index d6f6951c4..ab92ef7bf 100644 --- a/api/src/unraid-api/types/request.ts +++ b/api/src/unraid-api/types/request.ts @@ -1,3 +1,5 @@ import type { FastifyRequest } from '@app/types/fastify'; -export interface CustomRequest extends FastifyRequest {} +export interface CustomRequest extends FastifyRequest { + headers: FastifyRequest['headers'] & { 'x-csrf-token'?: string }; +} diff --git a/web/.env.example b/web/.env.example index bc4c86abf..adb0fc96b 100644 --- a/web/.env.example +++ b/web/.env.example @@ -10,5 +10,7 @@ VITE_ALLOW_CONSOLE_LOGS=true # For an Unraid Webgui deployment, set this to 10. VITE_TAILWIND_BASE_FONT_SIZE=16 VITE_WEBGUI=http://localhost:3001 +# static override for csrf token during development. +VITE_CSRF_TOKEN="0000000000000000" # Flag for mocking a user session during development via an unsecure cookie VITE_MOCK_USER_SESSION=true diff --git a/web/helpers/create-apollo-client.ts b/web/helpers/create-apollo-client.ts index a1a5b12be..175fc2d5c 100644 --- a/web/helpers/create-apollo-client.ts +++ b/web/helpers/create-apollo-client.ts @@ -20,7 +20,9 @@ const httpEndpoint = WEBGUI_GRAPHQL; const wsEndpoint = new URL(WEBGUI_GRAPHQL.toString().replace('http', 'ws')); // const headers = { 'x-api-key': serverStore.apiKey }; -const headers = {}; +const headers = { + 'x-csrf-token': globalThis.csrf_token, +}; const httpLink = createHttpLink({ uri: httpEndpoint.toString(), diff --git a/web/helpers/globals.d.ts b/web/helpers/globals.d.ts new file mode 100644 index 000000000..b1c3a4d80 --- /dev/null +++ b/web/helpers/globals.d.ts @@ -0,0 +1,7 @@ +declare global { + // eslint-disable-next-line no-var + var csrf_token: string; +} + +// an export or import statement is required to make this file a module +export {}; \ No newline at end of file diff --git a/web/helpers/urls.ts b/web/helpers/urls.ts index 36ccea357..ed34b4c26 100644 --- a/web/helpers/urls.ts +++ b/web/helpers/urls.ts @@ -40,6 +40,11 @@ const DOCS_REGISTRATION_REPLACE_KEY = new URL('/go/changing-the-flash-device/', const SUPPORT = new URL('https://unraid.net'); +// initialize csrf_token in nuxt playground +if (import.meta.env.VITE_CSRF_TOKEN) { + globalThis.csrf_token = import.meta.env.VITE_CSRF_TOKEN; +} + export { ACCOUNT, ACCOUNT_CALLBACK,