[server/xsrf] use the current version as the xsrf header, rather than a random value

This commit is contained in:
spalger 2015-12-07 14:11:19 -07:00
parent 5ee11c894b
commit 83d1cde0ed
7 changed files with 95 additions and 131 deletions

View file

@ -43,7 +43,6 @@ module.exports = () => Joi.object({
otherwise: Joi.boolean().default(false)
}),
xsrf: Joi.object({
token: Joi.string().default(randomBytes(32).toString('hex')),
disableProtection: Joi.boolean().default(false),
}).default(),
}).default(),

View file

@ -8,6 +8,9 @@ const nonDestructiveMethods = ['GET'];
const destructiveMethods = ['POST', 'PUT', 'DELETE'];
const src = resolve.bind(null, __dirname, '../../../../src');
const xsrfHeader = 'kbn-version';
const version = require(src('../package.json')).version;
describe('xsrf request filter', function () {
function inject(kbnServer, opts) {
return fn(cb => {
@ -17,9 +20,9 @@ describe('xsrf request filter', function () {
});
}
const makeServer = async function (token) {
const makeServer = async function () {
const kbnServer = new KbnServer({
server: { autoListen: false, xsrf: { token } },
server: { autoListen: false },
plugins: { scanDirs: [src('plugins')] },
logging: { quiet: true },
optimize: { enabled: false },
@ -41,108 +44,74 @@ describe('xsrf request filter', function () {
return kbnServer;
};
describe('issuing tokens', function () {
const token = 'secur3';
let kbnServer;
beforeEach(async () => kbnServer = await makeServer(token));
afterEach(async () => await kbnServer.close());
let kbnServer;
beforeEach(async () => kbnServer = await makeServer());
afterEach(async () => await kbnServer.close());
it('sends a token when rendering an app', async function () {
var resp = await inject(kbnServer, {
method: 'GET',
url: '/app/kibana',
for (const method of nonDestructiveMethods) {
context(`nonDestructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func
it('accepts requests without a token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method
});
expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});
expect(resp.payload).to.contain(`"xsrfToken":"${token}"`);
it('failes on invalid tokens', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
[xsrfHeader]: `invalid:${version}`,
},
});
expect(resp.statusCode).to.be(400);
expect(resp.headers).to.have.property(xsrfHeader, version);
});
});
});
}
context('without configured token', function () {
let kbnServer;
beforeEach(async () => kbnServer = await makeServer());
afterEach(async () => await kbnServer.close());
for (const method of destructiveMethods) {
context(`destructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func
it('accepts requests with the correct token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
[xsrfHeader]: version,
},
});
it('responds with a random token', async function () {
var resp = await inject(kbnServer, {
method: 'GET',
url: '/app/kibana',
expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});
expect(resp.payload).to.match(/"xsrfToken":".{64}"/);
it('rejects requests without a token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method
});
expect(resp.statusCode).to.be(400);
expect(resp.payload).to.match(/"Missing kbn-version header/);
});
it('rejects requests with an invalid token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
[xsrfHeader]: `invalid:${version}`,
},
});
expect(resp.statusCode).to.be(400);
expect(resp.payload).to.match(/"Invalid kbn-version, expected/);
});
});
});
context('with configured token', function () {
const token = 'mytoken';
let kbnServer;
beforeEach(async () => kbnServer = await makeServer(token));
afterEach(async () => await kbnServer.close());
for (const method of nonDestructiveMethods) {
context(`nonDestructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func
it('accepts requests without a token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method
});
expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});
it('ignores invalid tokens', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
'kbn-xsrf-token': `invalid:${token}`,
},
});
expect(resp.statusCode).to.be(200);
expect(resp.headers).to.not.have.property('kbn-xsrf-token');
});
});
}
for (const method of destructiveMethods) {
context(`destructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func
it('accepts requests with the correct token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
'kbn-xsrf-token': token,
},
});
expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});
it('rejects requests without a token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method
});
expect(resp.statusCode).to.be(403);
expect(resp.payload).to.match(/"Missing XSRF token"/);
});
it('rejects requests with an invalid token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
'kbn-xsrf-token': `invalid:${token}`,
},
});
expect(resp.statusCode).to.be(403);
expect(resp.payload).to.match(/"Invalid XSRF token"/);
});
});
}
});
}
});

View file

@ -114,11 +114,11 @@ module.exports = function (kbnServer, server, config) {
let response = req.response;
if (response.isBoom) {
response.output.headers['x-app-name'] = kbnServer.name;
response.output.headers['x-app-version'] = kbnServer.version;
response.output.headers['kbn-name'] = kbnServer.name;
response.output.headers['kbn-version'] = kbnServer.version;
} else {
response.header('x-app-name', kbnServer.name);
response.header('x-app-version', kbnServer.version);
response.header('kbn-name', kbnServer.name);
response.header('kbn-version', kbnServer.version);
}
return reply.continue();

View file

@ -1,19 +1,17 @@
import { forbidden } from 'boom';
import { badRequest } from 'boom';
export default function (kbnServer, server, config) {
const token = config.get('server.xsrf.token');
const version = config.get('pkg.version');
const disabled = config.get('server.xsrf.disableProtection');
server.decorate('reply', 'issueXsrfToken', function () {
return token;
});
const header = 'kbn-version';
server.ext('onPostAuth', function (req, reply) {
if (disabled || req.method === 'get') return reply.continue();
const noHeaderGet = req.method === 'get' && !req.headers[header];
if (disabled || noHeaderGet) return reply.continue();
const attempt = req.headers['kbn-xsrf-token'];
if (!attempt) return reply(forbidden('Missing XSRF token'));
if (attempt !== token) return reply(forbidden('Invalid XSRF token'));
const submission = req.headers[header];
if (!submission) return reply(badRequest(`Missing ${header} header`));
if (submission !== version) return reply(badRequest(`Invalid ${header}, expected ${version}`, { version }));
return reply.continue();
});

View file

@ -75,7 +75,6 @@ module.exports = async (kbnServer, server, config) => {
buildSha: config.get('pkg.buildSha'),
basePath: config.get('server.basePath'),
vars: defaults(app.getInjectedVars(), defaultInjectedVars),
xsrfToken: this.issueXsrfToken(),
};
return this.view(app.templateName, {

View file

@ -5,43 +5,42 @@ import ngMock from 'ngMock';
import xsrfChromeApi from '../xsrf';
const xsrfHeader = 'kbn-xsrf-token';
const xsrfToken = 'xsrfToken';
const xsrfHeader = 'kbn-version';
const { version } = require('../../../../../../package.json');
describe('chrome xsrf apis', function () {
describe('#getXsrfToken()', function () {
it('exposes the token', function () {
const chrome = {};
xsrfChromeApi(chrome, { xsrfToken });
expect(chrome.getXsrfToken()).to.be(xsrfToken);
xsrfChromeApi(chrome, { version });
expect(chrome.getXsrfToken()).to.be(version);
});
});
context('jQuery support', function () {
it('adds a global jQuery prefilter', function () {
stub($, 'ajaxPrefilter');
xsrfChromeApi({}, {});
xsrfChromeApi({}, { version });
expect($.ajaxPrefilter.callCount).to.be(1);
});
context('jQuery prefilter', function () {
let prefilter;
const xsrfToken = 'xsrfToken';
beforeEach(function () {
stub($, 'ajaxPrefilter');
xsrfChromeApi({}, { xsrfToken });
xsrfChromeApi({}, { version });
prefilter = $.ajaxPrefilter.args[0][0];
});
it('sets the kbn-xsrf-token header', function () {
it(`sets the ${xsrfHeader} header`, function () {
const setHeader = stub();
prefilter({}, {}, { setRequestHeader: setHeader });
expect(setHeader.callCount).to.be(1);
expect(setHeader.args[0]).to.eql([
xsrfHeader,
xsrfToken
version
]);
});
@ -60,7 +59,7 @@ describe('chrome xsrf apis', function () {
beforeEach(function () {
stub($, 'ajaxPrefilter');
const chrome = {};
xsrfChromeApi(chrome, { xsrfToken });
xsrfChromeApi(chrome, { version });
ngMock.module(chrome.$setupXsrfRequestInterceptor);
});
@ -78,9 +77,9 @@ describe('chrome xsrf apis', function () {
$httpBackend.verifyNoOutstandingRequest();
});
it('injects a kbn-xsrf-token header on every request', function () {
it(`injects a ${xsrfHeader} header on every request`, function () {
$httpBackend.expectPOST('/api/test', undefined, function (headers) {
return headers[xsrfHeader] === xsrfToken;
return headers[xsrfHeader] === version;
}).respond(200, '');
$http.post('/api/test');
@ -114,7 +113,7 @@ describe('chrome xsrf apis', function () {
});
it('accepts alternate tokens to use', function () {
const customToken = `custom:${xsrfToken}`;
const customToken = `custom:${version}`;
$httpBackend.expectPOST('/api/test', undefined, function (headers) {
return headers[xsrfHeader] === customToken;
}).respond(200, '');

View file

@ -4,12 +4,12 @@ import { set } from 'lodash';
export default function (chrome, internals) {
chrome.getXsrfToken = function () {
return internals.xsrfToken;
return internals.version;
};
$.ajaxPrefilter(function ({ kbnXsrfToken = internals.xsrfToken }, originalOptions, jqXHR) {
$.ajaxPrefilter(function ({ kbnXsrfToken = internals.version }, originalOptions, jqXHR) {
if (kbnXsrfToken) {
jqXHR.setRequestHeader('kbn-xsrf-token', kbnXsrfToken);
jqXHR.setRequestHeader('kbn-version', kbnXsrfToken);
}
});
@ -17,9 +17,9 @@ export default function (chrome, internals) {
$httpProvider.interceptors.push(function () {
return {
request: function (opts) {
const { kbnXsrfToken = internals.xsrfToken } = opts;
const { kbnXsrfToken = internals.version } = opts;
if (kbnXsrfToken) {
set(opts, ['headers', 'kbn-xsrf-token'], kbnXsrfToken);
set(opts, ['headers', 'kbn-version'], kbnXsrfToken);
}
return opts;
}