diff --git a/CHANGELOG.md b/CHANGELOG.md index 9de1bf906e..b7d968ddee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- [#2150: Prevent CSURF deprecated warning](https://github.com/alphagov/govuk-prototype-kit/pull/2150) + ## 13.6.2 ### Fixes diff --git a/cypress/e2e/plugins/1-available-plugins-tests/install-available-plugin.cypress.js b/cypress/e2e/plugins/1-available-plugins-tests/install-available-plugin.cypress.js index b7e9dd3b2c..24568286c8 100644 --- a/cypress/e2e/plugins/1-available-plugins-tests/install-available-plugin.cypress.js +++ b/cypress/e2e/plugins/1-available-plugins-tests/install-available-plugin.cypress.js @@ -63,7 +63,7 @@ describe('Management plugins: ', () => { body: { package: plugin } }).then(response => { expect(response.status).to.eq(403) - expect(response.body).to.eq('invalid csrf token') + expect(response.body).to.have.property('error', 'invalid csrf token') }) }) diff --git a/lib/assets/javascripts/manage-prototype/manage-plugins.js b/lib/assets/javascripts/manage-prototype/manage-plugins.js index 26bc718b19..f12b81a3c5 100644 --- a/lib/assets/javascripts/manage-prototype/manage-plugins.js +++ b/lib/assets/javascripts/manage-prototype/manage-plugins.js @@ -38,14 +38,13 @@ show('panel-error') } - const postRequest = (url) => { - const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content') + const postRequest = (url, token) => { return fetch(url, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', - 'CSRF-Token': token + 'X-CSRF-TOKEN': token }, body: JSON.stringify({ package: packageName, @@ -102,6 +101,12 @@ }) } + const getToken = () => { + return fetch('/manage-prototype/csrf-token') + .then(response => response.json()) + .then(({ token }) => token) + } + const performAction = (event) => { if (!actionTimeoutId) { actionTimeoutId = setTimeout(() => { @@ -116,7 +121,8 @@ show('panel-processing') - return postRequest(`/manage-prototype/plugins/${mode}`) + return getToken() + .then((token) => postRequest(`/manage-prototype/plugins/${mode}`, token)) .then(data => { switch (data.status) { case 'completed': diff --git a/lib/assets/javascripts/manage-prototype/manage-plugins.test.js b/lib/assets/javascripts/manage-prototype/manage-plugins.test.js index 4b34e57c2e..3a1a61185f 100644 --- a/lib/assets/javascripts/manage-prototype/manage-plugins.test.js +++ b/lib/assets/javascripts/manage-prototype/manage-plugins.test.js @@ -48,9 +48,12 @@ const errorHTML = ` ` +const token = 'csrf-test-token' + describe('manage-plugins', () => { global.fetch = jest.fn().mockResolvedValue(null) + const getTokenUrl = '/manage-prototype/csrf-token' const performActionUrl = '/manage-prototype/plugins/' const pollStatusUrl = '/manage-prototype/plugins//status' @@ -92,7 +95,6 @@ describe('manage-plugins', () => { } return 'action-timeout-id' }) - document.head.innerHTML = '' document.body.innerHTML = loadedHTML fetchList = [] }) @@ -119,33 +121,57 @@ describe('manage-plugins', () => { }) it('completed', async () => { - mockFetch(['completed']) + mockFetch([ + { token }, + 'completed' + ]) await managePlugins.performAction() expect(document.body.innerHTML).toEqual(completedHTML) - expect(global.fetch).toHaveBeenCalledTimes(1) - expect(fetchList).toEqual([performActionUrl]) + expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) + expect(fetchList).toEqual([ + getTokenUrl, + performActionUrl + ]) }) it('error', async () => { - mockFetch(['error']) + mockFetch([ + { token }, + 'error' + ]) await managePlugins.performAction() expect(document.body.innerHTML).toEqual(errorHTML) - expect(global.fetch).toHaveBeenCalledTimes(1) - expect(fetchList).toEqual([performActionUrl]) + expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) + expect(fetchList).toEqual([ + getTokenUrl, + performActionUrl + ]) }) it('will restart', async () => { - mockFetch(['throws', 'processing', 'completed']) + mockFetch([ + { token }, + 'throws', + { token }, + 'processing', + 'completed'] + ) await managePlugins.performAction() expect(document.body.innerHTML).toEqual(completedHTML) - expect(global.fetch).toHaveBeenCalledTimes(3) - expect(fetchList).toEqual([performActionUrl, performActionUrl, pollStatusUrl]) + expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) + expect(fetchList).toEqual([ + getTokenUrl, + performActionUrl, + getTokenUrl, + performActionUrl, + pollStatusUrl + ]) }) }) @@ -164,8 +190,12 @@ describe('manage-plugins', () => { await managePlugins.pollStatus() expect(document.body.innerHTML).toEqual(completedHTML) - expect(global.fetch).toHaveBeenCalledTimes(3) - expect(fetchList).toEqual([pollStatusUrl, pollStatusUrl, pollStatusUrl]) + expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) + expect(fetchList).toEqual([ + pollStatusUrl, + pollStatusUrl, + pollStatusUrl + ]) }) it('error', async () => { @@ -178,8 +208,12 @@ describe('manage-plugins', () => { await managePlugins.pollStatus() expect(document.body.innerHTML).toEqual(errorHTML) - expect(global.fetch).toHaveBeenCalledTimes(3) - expect(fetchList).toEqual([pollStatusUrl, pollStatusUrl, pollStatusUrl]) + expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) + expect(fetchList).toEqual([ + pollStatusUrl, + pollStatusUrl, + pollStatusUrl + ]) }) }) @@ -190,6 +224,7 @@ describe('manage-plugins', () => { it('completed', async () => { mockFetch([ + { token }, 'processing', 'processing', 'throws', @@ -199,12 +234,19 @@ describe('manage-plugins', () => { await managePlugins.init() expect(document.body.innerHTML).toEqual(completedHTML) - expect(global.fetch).toHaveBeenCalledTimes(4) - expect(fetchList).toEqual([performActionUrl, pollStatusUrl, pollStatusUrl, pollStatusUrl]) + expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) + expect(fetchList).toEqual([ + getTokenUrl, + performActionUrl, + pollStatusUrl, + pollStatusUrl, + pollStatusUrl + ]) }) it('error', async () => { mockFetch([ + { token }, 'processing', 'processing', 'throws', @@ -214,8 +256,14 @@ describe('manage-plugins', () => { await managePlugins.init() expect(document.body.innerHTML).toEqual(errorHTML) - expect(global.fetch).toHaveBeenCalledTimes(4) - expect(fetchList).toEqual([performActionUrl, pollStatusUrl, pollStatusUrl, pollStatusUrl]) + expect(global.fetch).toHaveBeenCalledTimes(fetchList.length) + expect(fetchList).toEqual([ + getTokenUrl, + performActionUrl, + pollStatusUrl, + pollStatusUrl, + pollStatusUrl + ]) }) }) }) diff --git a/lib/manage-prototype-handlers.js b/lib/manage-prototype-handlers.js index 335c4901c9..39d22069ae 100644 --- a/lib/manage-prototype-handlers.js +++ b/lib/manage-prototype-handlers.js @@ -5,6 +5,7 @@ const path = require('path') // npm dependencies const fse = require('fs-extra') const querystring = require('querystring') +const { doubleCsrf } = require('csrf-csrf') // local dependencies const config = require('./config') @@ -79,6 +80,27 @@ function getManagementView (filename) { // Local dependencies const encryptPassword = require('./utils').encryptPassword +const { invalidCsrfTokenError, generateToken, doubleCsrfProtection } = doubleCsrf({ + getSecret: (req) => 'Secret', + cookieName: 'x-csrf-token' +}) + +// Error handling, validation error interception +const csrfErrorHandler = (error, req, res, next) => { + if (error === invalidCsrfTokenError) { + res.status(403).json({ + error: 'invalid csrf token' + }) + } else { + next() + } +} + +function getCsrfTokenHandler (req, res) { + const token = generateToken(res, req) + return res.json({ token }) +} + // Clear all data in session function getClearDataHandler (req, res) { res.render(getManagementView('clear-data.njk')) @@ -562,7 +584,6 @@ async function getPluginsModeHandler (req, res) { command: getCommand(mode, chosenPlugin), verb, isSameOrigin, - csrfToken: req.csrfToken(), returnLink }) } @@ -658,6 +679,8 @@ async function postPluginsModeHandler (req, res) { module.exports = { contextPath, setKitRestarted, + csrfProtection: [doubleCsrfProtection, csrfErrorHandler], + getCsrfTokenHandler, getClearDataHandler, postClearDataHandler, getPasswordHandler, diff --git a/lib/manage-prototype-handlers.test.js b/lib/manage-prototype-handlers.test.js index 63b008c190..795d5a21e4 100644 --- a/lib/manage-prototype-handlers.test.js +++ b/lib/manage-prototype-handlers.test.js @@ -355,7 +355,7 @@ describe('manage-prototype-handlers', () => { }) describe('plugins handlers', () => { - const csrfToken = 'CSRF-TOKEN' + const csrfToken = 'x-csrf-token' const packageName = 'test-package' const latestVersion = '2.0.0' const previousVersion = '1.0.0' @@ -412,7 +412,6 @@ describe('manage-prototype-handlers', () => { expect.objectContaining({ chosenPlugin: availablePlugin, command: `npm install ${packageName} --save-exact`, - csrfToken, currentSection: 'Plugins', pageName: `Install ${pluginDisplayName.name}`, currentUrl: req.originalUrl, diff --git a/lib/manage-prototype-routes.js b/lib/manage-prototype-routes.js index 3d03b5dfdb..913fbd868e 100644 --- a/lib/manage-prototype-routes.js +++ b/lib/manage-prototype-routes.js @@ -1,12 +1,11 @@ // npm dependencies -const csrf = require('csurf') const express = require('express') -const csrfProtection = csrf({ cookie: false }) - const { contextPath, setKitRestarted, + csrfProtection, + getCsrfTokenHandler, getClearDataHandler, postClearDataHandler, getPasswordHandler, @@ -34,6 +33,8 @@ redirectingRouter.use((req, res) => { res.redirect(req.originalUrl.replace('/manage-prototype', contextPath)) }) +router.get('/csrf-token', getCsrfTokenHandler) + // Clear all data in session router.get('/clear-data', getClearDataHandler) diff --git a/lib/nunjucks/views/manage-prototype/plugin-install-or-uninstall.njk b/lib/nunjucks/views/manage-prototype/plugin-install-or-uninstall.njk index 00e33a0c19..b7523eaf5a 100644 --- a/lib/nunjucks/views/manage-prototype/plugin-install-or-uninstall.njk +++ b/lib/nunjucks/views/manage-prototype/plugin-install-or-uninstall.njk @@ -1,10 +1,6 @@ {% extends "views/manage-prototype/layout.njk" %} {% from "govuk/components/button/macro.njk" import govukButton %} -{% block meta %} - -{% endblock %} - {% block content %}
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index c32cacb3a7..8afc90fbf5 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -14,7 +14,7 @@ "chokidar": "^3.5.3", "cookie-parser": "^1.4.6", "cross-spawn": "^7.0.3", - "csurf": "^1.11.0", + "csrf-csrf": "^2.2.4", "del": "^6.1.1", "dotenv": "^16.0.3", "express": "^4.18.2", @@ -3589,17 +3589,12 @@ "node": ">= 8" } }, - "node_modules/csrf": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", - "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "node_modules/csrf-csrf": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-2.2.4.tgz", + "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRT/YMl1Njo/yKh2L9AYsIwSlTPnx2A==", "dependencies": { - "rndm": "1.2.0", - "tsscmp": "1.0.6", - "uid-safe": "2.1.5" - }, - "engines": { - "node": ">= 0.8" + "http-errors": "^2.0.0" } }, "node_modules/cssom": { @@ -3626,62 +3621,6 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, - "node_modules/csurf": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", - "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", - "deprecated": "Please use another csrf package", - "dependencies": { - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "csrf": "3.1.0", - "http-errors": "~1.7.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/csurf/node_modules/cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/csurf/node_modules/http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/csurf/node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/csurf/node_modules/setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "node_modules/csurf/node_modules/toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "engines": { - "node": ">=0.6" - } - }, "node_modules/cypress": { "version": "12.3.0", "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.3.0.tgz", @@ -10666,11 +10605,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rndm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", - "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==" - }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -11901,14 +11835,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, - "node_modules/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "engines": { - "node": ">=0.6.x" - } - }, "node_modules/tsutils": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", @@ -15293,14 +15219,12 @@ "which": "^2.0.1" } }, - "csrf": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", - "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "csrf-csrf": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-2.2.4.tgz", + "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRT/YMl1Njo/yKh2L9AYsIwSlTPnx2A==", "requires": { - "rndm": "1.2.0", - "tsscmp": "1.0.6", - "uid-safe": "2.1.5" + "http-errors": "^2.0.0" } }, "cssom": { @@ -15326,51 +15250,6 @@ } } }, - "csurf": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", - "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", - "requires": { - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "csrf": "3.1.0", - "http-errors": "~1.7.3" - }, - "dependencies": { - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - } - } - }, "cypress": { "version": "12.3.0", "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.3.0.tgz", @@ -20445,11 +20324,6 @@ } } }, - "rndm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", - "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==" - }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -21386,11 +21260,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, - "tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" - }, "tsutils": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", diff --git a/package.json b/package.json index e93364a02d..44f1a58fa3 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "chokidar": "^3.5.3", "cookie-parser": "^1.4.6", "cross-spawn": "^7.0.3", - "csurf": "^1.11.0", + "csrf-csrf": "^2.2.4", "del": "^6.1.1", "dotenv": "^16.0.3", "express": "^4.18.2", diff --git a/server.js b/server.js index 761a14854b..37b9c2595b 100644 --- a/server.js +++ b/server.js @@ -60,6 +60,9 @@ app.locals.pluginConfig = plugins.getAppConfig({ // TODO: remove in v14 app.locals.extensionConfig = app.locals.pluginConfig +// Support session data storage +app.use(sessionUtils.getSessionMiddleware()) + // use cookie middleware for reading authentication cookie app.use(cookieParser()) @@ -67,9 +70,6 @@ app.use(cookieParser()) // static assets to prevent unauthorised access app.use(require('./lib/authentication.js')()) -// Support session data storage -app.use(sessionUtils.getSessionMiddleware()) - // Get internal govuk-frontend views const internalGovUkFrontendDir = getInternalGovukFrontendDir() const internalGovUkFrontendConfig = fse.readJsonSync(path.join(internalGovUkFrontendDir, 'govuk-prototype-kit.config.json'))