diff --git a/src/config/index.ts b/src/config/index.ts index 96fe12dac..e60acc61d 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -55,7 +55,7 @@ export const config = convict({ }, enforceCsrf: { format: Boolean, - default: isProduction, + default: true, env: 'ENFORCE_CSRF' }, diff --git a/src/server/plugins/engine/helpers.test.ts b/src/server/plugins/engine/helpers.test.ts index 6d8abc603..df953dfae 100644 --- a/src/server/plugins/engine/helpers.test.ts +++ b/src/server/plugins/engine/helpers.test.ts @@ -13,7 +13,6 @@ import { getExponentialBackoffDelay, getPageHref, proceed, - safeGenerateCrumb, type GlobalScope } from '~/src/server/plugins/engine/helpers.js' import { handleLegacyRedirect } from '~/src/server/plugins/engine/helpers.js' @@ -30,7 +29,6 @@ import { import { FormAction, FormStatus, - type FormRequest, type FormResponseToolkit } from '~/src/server/routes/types.js' import definition from '~/test/form/definitions/basic.js' @@ -446,78 +444,6 @@ describe('Helpers', () => { }) }) - describe('safeGenerateCrumb', () => { - it('should return undefined when request.state is missing (malformed request)', () => { - const malformedRequest = { - server: { - plugins: { - crumb: { - generate: jest.fn() - } - } - }, - plugins: {}, - route: { settings: { plugins: {} } }, - path: '/test', - url: { search: '' } - // state intentionally omitted - } as unknown as FormRequest - - const crumbToken = safeGenerateCrumb(malformedRequest) - expect(crumbToken).toBeUndefined() - expect( - malformedRequest.server.plugins.crumb.generate - ).not.toHaveBeenCalled() - }) - - it('should return undefined if crumb is disabled in route settings', () => { - const requestWithDisabledCrumb = { - server: { - plugins: { - crumb: { - generate: jest.fn().mockReturnValue('test-token') - } - } - }, - plugins: {}, - route: { settings: { plugins: { crumb: false } } }, - path: '/test', - url: { search: '' }, - state: {} - } as unknown as FormRequest - - const crumbToken = safeGenerateCrumb(requestWithDisabledCrumb) - expect(crumbToken).toBeUndefined() - expect( - requestWithDisabledCrumb.server.plugins.crumb.generate - ).not.toHaveBeenCalled() - }) - - it('should generate crumb when state exists and crumb plugin is available', () => { - const mockCrumb = 'generated-crumb-value' - const validRequest = { - server: { - plugins: { - crumb: { - generate: jest.fn().mockReturnValue(mockCrumb) - } - } - }, - plugins: {}, - route: { settings: { plugins: {} } }, - path: '/test', - url: { search: '' }, - state: {} - } as unknown as FormRequest - - const crumbToken = safeGenerateCrumb(validRequest) - expect(crumbToken).toBe(mockCrumb) - expect(validRequest.server.plugins.crumb.generate).toHaveBeenCalledWith( - validRequest - ) - }) - }) - describe('getExponentialBackoffDelay', () => { it.each([ { depth: 1, expected: 2000 }, diff --git a/src/server/plugins/engine/helpers.ts b/src/server/plugins/engine/helpers.ts index b57c893cb..20356f83d 100644 --- a/src/server/plugins/engine/helpers.ts +++ b/src/server/plugins/engine/helpers.ts @@ -23,7 +23,6 @@ import { import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js' import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js' import { - type AnyFormRequest, type FormContext, type FormContextRequest, type FormSubmissionError @@ -320,32 +319,6 @@ export function getError(detail: ValidationErrorItem): FormSubmissionError { } } -/** - * A small helper to safely generate a crumb token. - * Checks that the crumb plugin is available, that crumb - * is not disabled on the current route, and that cookies/state are present. - */ -export function safeGenerateCrumb( - request: AnyFormRequest | null -): string | undefined { - // no request or no .state - if (!request?.state) { - return undefined - } - - // crumb plugin or its generate method doesn't exist - if (!request.server.plugins.crumb.generate) { - return undefined - } - - // crumb is explicitly disabled for this route - if (request.route.settings.plugins?.crumb === false) { - return undefined - } - - return request.server.plugins.crumb.generate(request) -} - /** * Calculates an exponential backoff delay (in milliseconds) based on the current retry depth, * using a base delay of 2000ms (2 seconds) and doubling for each additional depth, while capping the delay at 25,000ms (25 seconds). diff --git a/src/server/plugins/nunjucks/context.js b/src/server/plugins/nunjucks/context.js index ef5b9dfca..9634e6720 100644 --- a/src/server/plugins/nunjucks/context.js +++ b/src/server/plugins/nunjucks/context.js @@ -8,8 +8,7 @@ import { config } from '~/src/config/index.js' import { createLogger } from '~/src/server/common/helpers/logging/logger.js' import { checkFormStatus, - encodeUrl, - safeGenerateCrumb + encodeUrl } from '~/src/server/plugins/engine/helpers.js' const logger = createLogger() @@ -51,7 +50,6 @@ export async function context(request) { // take consumers props first so we can override it ...consumerViewContext, baseLayoutPath: pluginStorage.baseLayoutPath, - crumb: safeGenerateCrumb(request), currentPath: `${request.path}${request.url.search}`, previewMode: isPreviewMode ? formState : undefined, slug: isResponseOK ? params?.slug : undefined diff --git a/src/server/plugins/nunjucks/context.test.js b/src/server/plugins/nunjucks/context.test.js index 35fec85f8..a54f8bcd8 100644 --- a/src/server/plugins/nunjucks/context.test.js +++ b/src/server/plugins/nunjucks/context.test.js @@ -98,39 +98,6 @@ describe('Nunjucks context', () => { malformedRequest.server.plugins.crumb.generate ).not.toHaveBeenCalled() }) - - it('should generate crumb when state exists', async () => { - const mockCrumb = 'generated-crumb-value' - const validRequest = /** @type {FormRequest} */ ( - /** @type {unknown} */ ({ - server: { - plugins: { - crumb: { - generate: jest.fn().mockReturnValue(mockCrumb) - }, - 'forms-engine-plugin': { - baseLayoutPath: 'randomValue' - } - } - }, - plugins: {}, - route: { - settings: { - plugins: {} - } - }, - path: '/test', - url: { search: '' }, - state: {} - }) - ) - - const { crumb } = await context(validRequest) - expect(crumb).toBe(mockCrumb) - expect(validRequest.server.plugins.crumb.generate).toHaveBeenCalledWith( - validRequest - ) - }) }) }) diff --git a/src/server/routes/dummy-api.test.ts b/src/server/routes/dummy-api.test.ts index 02ba1d41b..8e6e257ba 100644 --- a/src/server/routes/dummy-api.test.ts +++ b/src/server/routes/dummy-api.test.ts @@ -22,7 +22,9 @@ describe('Dummy API', () => { beforeAll(async () => { MockDate.set('2025-01-01T00:00:00Z') - server = await createServer() + server = await createServer({ + enforceCsrf: false + }) await server.initialize() }) diff --git a/test/condition/checkboxes.test.js b/test/condition/checkboxes.test.js index 4e3f943f3..32acd362c 100644 --- a/test/condition/checkboxes.test.js +++ b/test/condition/checkboxes.test.js @@ -21,7 +21,8 @@ describe('Checkboxes based conditions', () => { beforeAll(async () => { server = await createServer({ formFileName: 'checkboxes.json', - formFilePath: resolve(import.meta.dirname, '../form/definitions') + formFilePath: resolve(import.meta.dirname, '../form/definitions'), + enforceCsrf: false }) await server.initialize() diff --git a/test/condition/radios.test.js b/test/condition/radios.test.js index 4290eab42..2db41cd59 100644 --- a/test/condition/radios.test.js +++ b/test/condition/radios.test.js @@ -21,7 +21,8 @@ describe('Radio based conditions', () => { beforeAll(async () => { server = await createServer({ formFileName: 'radios.json', - formFilePath: resolve(import.meta.dirname, '../form/definitions') + formFilePath: resolve(import.meta.dirname, '../form/definitions'), + enforceCsrf: false }) await server.initialize() diff --git a/test/condition/text.test.js b/test/condition/text.test.js index 473895757..cac6a4e7a 100644 --- a/test/condition/text.test.js +++ b/test/condition/text.test.js @@ -21,7 +21,8 @@ describe('TextField based conditions', () => { beforeAll(async () => { server = await createServer({ formFileName: 'text.json', - formFilePath: resolve(import.meta.dirname, '../form/definitions') + formFilePath: resolve(import.meta.dirname, '../form/definitions'), + enforceCsrf: false }) await server.initialize() diff --git a/test/form/file-upload.test.js b/test/form/file-upload.test.js index 3402877ef..a54b6f007 100644 --- a/test/form/file-upload.test.js +++ b/test/form/file-upload.test.js @@ -174,7 +174,8 @@ describe('File upload POST tests', () => { beforeAll(async () => { server = await createServer({ formFileName: 'file-upload.js', - formFilePath: resolve(import.meta.dirname, '../form/definitions') + formFilePath: resolve(import.meta.dirname, '../form/definitions'), + enforceCsrf: false }) await server.initialize() diff --git a/test/form/govuk-notify.test.js b/test/form/govuk-notify.test.js index 7a4b1b3b7..ff8bbd2b4 100644 --- a/test/form/govuk-notify.test.js +++ b/test/form/govuk-notify.test.js @@ -74,7 +74,8 @@ describe('Submission journey test', () => { beforeAll(async () => { server = await createServer({ formFileName: 'components.json', - formFilePath: join(import.meta.dirname, 'definitions') + formFilePath: join(import.meta.dirname, 'definitions'), + enforceCsrf: false }) await server.initialize() diff --git a/test/form/joined-conditions.test.js b/test/form/joined-conditions.test.js index fe136a4c8..b4c96acea 100644 --- a/test/form/joined-conditions.test.js +++ b/test/form/joined-conditions.test.js @@ -20,7 +20,8 @@ describe('Joined conditions functional tests', () => { beforeAll(async () => { server = await createServer({ formFileName: 'joined-conditions-simple-v2.js', - formFilePath: resolve(import.meta.dirname, 'definitions') + formFilePath: resolve(import.meta.dirname, 'definitions'), + enforceCsrf: false }) await server.initialize() }) @@ -270,7 +271,8 @@ describe('Joined conditions functional tests', () => { beforeAll(async () => { server = await createServer({ formFileName: 'joined-conditions-complex-v2.js', - formFilePath: resolve(import.meta.dirname, 'definitions') + formFilePath: resolve(import.meta.dirname, 'definitions'), + enforceCsrf: false }) await server.initialize() }) diff --git a/test/form/persist-files.test.js b/test/form/persist-files.test.js index 134a84703..8102998c2 100644 --- a/test/form/persist-files.test.js +++ b/test/form/persist-files.test.js @@ -75,7 +75,8 @@ describe('Submission journey test', () => { beforeAll(async () => { server = await createServer({ formFileName: 'file-upload-basic.js', - formFilePath: join(import.meta.dirname, 'definitions') + formFilePath: join(import.meta.dirname, 'definitions'), + enforceCsrf: false }) await server.initialize() diff --git a/test/form/repeat.test.js b/test/form/repeat.test.js index 48f4b85b4..c2095aa9b 100644 --- a/test/form/repeat.test.js +++ b/test/form/repeat.test.js @@ -90,7 +90,8 @@ describe('Repeat GET tests', () => { beforeAll(async () => { server = await createServer({ formFileName: 'repeat.js', - formFilePath: resolve(import.meta.dirname, '../form/definitions') + formFilePath: resolve(import.meta.dirname, '../form/definitions'), + enforceCsrf: false }) const model = server.app.model @@ -363,7 +364,8 @@ describe('Repeat POST tests', () => { server = await createServer({ formFileName: 'repeat.js', formFilePath: resolve(import.meta.dirname, '../form/definitions'), - saveAndExit: (request, h, _context) => h.redirect('/my-save-and-exit') + saveAndExit: (request, h, _context) => h.redirect('/my-save-and-exit'), + enforceCsrf: false }) const model = server.app.model