* Send CSRF tokens over XHR

* Update events.js

* Update browser.js
This commit is contained in:
Kevin Heis 2020-09-28 09:44:14 -07:00 коммит произвёл GitHub
Родитель cc7bd5d701
Коммит c450d8d555
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 81 добавлений и 41 удалений

Просмотреть файл

@ -25,14 +25,4 @@
<link rel="stylesheet" href="/dist/index.css">
<link rel="alternate icon" type="image/png" href="/assets/images/site/favicon.png">
<link rel="icon" type="image/svg+xml" href="/assets/images/site/favicon.svg">
{% if page.relativePath contains "managing-your-work-on-github/disabling-project-boards-in-your-organization" %}
<!--TODO CSRF remove the outer check -->
{% if fastlyEnabled %}
<!-- https://www.fastly.com/blog/caching-uncacheable-csrf-security -->
<esi:include src="/csrf" />
{% else %}
<meta name="csrf-token" content="{{ csrfToken }}" />
{% endif %}
{% endif %}
</head>

Просмотреть файл

@ -1,3 +1,17 @@
export async function fillCsrf () {
const response = await fetch('/csrf', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
const json = response.ok ? await response.json() : {}
const meta = document.createElement('meta')
meta.setAttribute('name', 'csrf-token')
meta.setAttribute('content', json.token)
document.querySelector('head').append(meta)
}
export default function getCsrf () {
const csrfEl = document
.querySelector('meta[name="csrf-token"]')

Просмотреть файл

@ -13,6 +13,7 @@ import print from './print'
import localization from './localization'
import helpfulness from './helpfulness'
import experiment from './experiment'
import { fillCsrf } from './get-csrf'
document.addEventListener('DOMContentLoaded', () => {
displayPlatformSpecificContent()
@ -26,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => {
wrapCodeTerms()
print()
localization()
fillCsrf()
helpfulness()
experiment()
})

Просмотреть файл

@ -55,14 +55,6 @@ module.exports = async function contextualize (req, res, next) {
req.context.siteTree = siteTree
req.context.pages = pages
// To securely accept data from end users,
// we need to validate that they were actually on a docs page first.
// We'll put this token in the <head> and call on it when we send user data
// to the docs server, so we know the request came from someone on the page.
req.context.csrfToken = req.csrfToken()
req.context.fastlyEnabled = process.env.NODE_ENV === 'production' &&
req.hostname === 'docs.github.com'
return next()
}

Просмотреть файл

@ -8,7 +8,7 @@ router.get('/', (req, res) => {
'surrogate-control': 'private, no-store',
'cache-control': 'private, no-store'
})
res.send(`<meta name="csrf-token" content="${req.context.csrfToken}" />`)
res.json({ token: req.csrfToken() })
})
module.exports = router

Просмотреть файл

@ -1,5 +1,4 @@
module.exports = require('csurf')({
cookie: require('../lib/cookie-settings'),
ignoreMethods: ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']
// TODO CSRF edit this to include POST and PUT to require it
ignoreMethods: ['GET', 'HEAD', 'OPTIONS']
})

Просмотреть файл

@ -135,6 +135,13 @@ describe('helpfulness', () => {
})
})
describe('csrf meta', () => {
it('should have a csrf-token meta tag on the page', async () => {
await page.goto('http://localhost:4001/en/actions/getting-started-with-github-actions/about-github-actions')
await page.waitForSelector('meta[name="csrf-token"]')
})
})
async function getLocationObject (page) {
const location = await page.evaluate(() => {
return Promise.resolve(JSON.stringify(window.location, null, 2))

Просмотреть файл

@ -9,7 +9,7 @@ describe('GET /csrf', () => {
it('should render a non-cache include for CSRF token', async () => {
const res = await request(app).get('/csrf')
expect(res.status).toBe(200)
expect(res.text).toMatch(/^<meta name="csrf-token" content="(.*?)" \/>$/)
expect(res.body).toHaveProperty('token')
expect(res.headers['surrogate-control']).toBe('private, no-store')
expect(res.headers['cache-control']).toBe('private, no-store')
})

Просмотреть файл

@ -11,14 +11,23 @@ Airtable.mockImplementation(function () {
})
describe('POST /events', () => {
beforeEach(() => {
jest.setTimeout(60 * 1000)
let csrfToken = ''
let agent
beforeEach(async () => {
process.env.AIRTABLE_API_KEY = '$AIRTABLE_API_KEY$'
process.env.AIRTABLE_BASE_KEY = '$AIRTABLE_BASE_KEY$'
agent = request.agent(app)
const csrfRes = await agent.get('/csrf')
csrfToken = csrfRes.body.token
})
afterEach(() => {
delete process.env.AIRTABLE_API_KEY
delete process.env.AIRTABLE_BASE_KEY
csrfToken = ''
})
describe('HELPFULNESS', () => {
@ -32,82 +41,93 @@ describe('POST /events', () => {
}
it('should accept a valid object', () =>
request(app)
agent
.post('/events')
.send(example)
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(201)
)
it('should reject extra properties', () =>
request(app)
agent
.post('/events')
.send({ ...example, toothpaste: false })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should not accept if type is missing', () =>
request(app)
agent
.post('/events')
.send({ ...example, type: undefined })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should not accept if url is missing', () =>
request(app)
agent
.post('/events')
.send({ ...example, url: undefined })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should not accept if url is misformatted', () =>
request(app)
agent
.post('/events')
.send({ ...example, url: 'examplecom' })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should not accept if vote is missing', () =>
request(app)
agent
.post('/events')
.send({ ...example, vote: undefined })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should not accept if vote is not boolean', () =>
request(app)
agent
.post('/events')
.send({ ...example, vote: 'true' })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should not accept if email is misformatted', () =>
request(app)
agent
.post('/events')
.send({ ...example, email: 'testexample.com' })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should not accept if comment is not string', () =>
request(app)
agent
.post('/events')
.send({ ...example, comment: [] })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should not accept if category is not an option', () =>
request(app)
agent
.post('/events')
.send({ ...example, category: 'Fabulous' })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
})
@ -122,64 +142,79 @@ describe('POST /events', () => {
}
it('should accept a valid object', () =>
request(app)
agent
.post('/events')
.send(example)
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(201)
)
it('should reject extra fields', () =>
request(app)
agent
.post('/events')
.send({ ...example, toothpaste: false })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should require a long unique user-id', () =>
request(app)
agent
.post('/events')
.send({ ...example, 'user-id': 'short' })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should require a test', () =>
request(app)
agent
.post('/events')
.send({ ...example, test: undefined })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should require a valid group', () =>
request(app)
agent
.post('/events')
.send({ ...example, group: 'revolution' })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(400)
)
it('should default the success field', () =>
request(app)
agent
.post('/events')
.send({ ...example, success: undefined })
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(201)
)
})
})
describe('PUT /events/:id', () => {
beforeEach(() => {
jest.setTimeout(60 * 1000)
let csrfToken = ''
let agent
beforeEach(async () => {
process.env.AIRTABLE_API_KEY = '$AIRTABLE_API_KEY$'
process.env.AIRTABLE_BASE_KEY = '$AIRTABLE_BASE_KEY$'
agent = request.agent(app)
const csrfRes = await agent.get('/csrf')
csrfToken = csrfRes.body.token
})
afterEach(() => {
delete process.env.AIRTABLE_API_KEY
delete process.env.AIRTABLE_BASE_KEY
csrfToken = ''
})
const example = {
@ -192,10 +227,11 @@ describe('PUT /events/:id', () => {
}
it('should update an existing HELPFULNESS event', () =>
request(app)
agent
.put('/events/TESTID')
.send(example)
.set('Accept', 'application/json')
.set('csrf-token', csrfToken)
.expect(200)
)
})