Skip to content

Commit

Permalink
Merge pull request #2150 from alphagov/2129-fix-our-csrf-vulnerability
Browse files Browse the repository at this point in the history
Prevent CSURF deprecated warning
  • Loading branch information
BenSurgisonGDS authored May 5, 2023
2 parents 4df5036 + 02201e7 commit 21fd1c8
Show file tree
Hide file tree
Showing 11 changed files with 123 additions and 179 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- [#2150: Prevent CSURF deprecated warning](https://github.com/alphagov/govuk-prototype-kit/pull/2150)

## 13.6.2

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})

Expand Down
14 changes: 10 additions & 4 deletions lib/assets/javascripts/manage-prototype/manage-plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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':
Expand Down
84 changes: 66 additions & 18 deletions lib/assets/javascripts/manage-prototype/manage-plugins.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,12 @@ const errorHTML = `
</div>
`

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'

Expand Down Expand Up @@ -92,7 +95,6 @@ describe('manage-plugins', () => {
}
return 'action-timeout-id'
})
document.head.innerHTML = '<meta content="authenticity_token" name="csrf-token" />'
document.body.innerHTML = loadedHTML
fetchList = []
})
Expand All @@ -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
])
})
})

Expand All @@ -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 () => {
Expand All @@ -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
])
})
})

Expand All @@ -190,6 +224,7 @@ describe('manage-plugins', () => {

it('completed', async () => {
mockFetch([
{ token },
'processing',
'processing',
'throws',
Expand All @@ -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',
Expand All @@ -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
])
})
})
})
25 changes: 24 additions & 1 deletion lib/manage-prototype-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -562,7 +584,6 @@ async function getPluginsModeHandler (req, res) {
command: getCommand(mode, chosenPlugin),
verb,
isSameOrigin,
csrfToken: req.csrfToken(),
returnLink
})
}
Expand Down Expand Up @@ -658,6 +679,8 @@ async function postPluginsModeHandler (req, res) {
module.exports = {
contextPath,
setKitRestarted,
csrfProtection: [doubleCsrfProtection, csrfErrorHandler],
getCsrfTokenHandler,
getClearDataHandler,
postClearDataHandler,
getPasswordHandler,
Expand Down
3 changes: 1 addition & 2 deletions lib/manage-prototype-handlers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions lib/manage-prototype-routes.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
{% extends "views/manage-prototype/layout.njk" %}
{% from "govuk/components/button/macro.njk" import govukButton %}

{% block meta %}
<meta name="csrf-token" content="{{ csrfToken }}">
{% endblock %}

{% block content %}
<div class="govuk-grid-row govuk-prototype-kit-manage-prototype-plugin-processing">
<div class="govuk-grid-column-two-thirds">
Expand Down
Loading

0 comments on commit 21fd1c8

Please sign in to comment.