Skip to content

Commit

Permalink
Merge pull request elastic#8151 from epixa/headercheck
Browse files Browse the repository at this point in the history
Separate xsrf handling and version checking
  • Loading branch information
epixa authored Sep 6, 2016
2 parents 7a1745c + 621bf0e commit ec3abde
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 30 deletions.
76 changes: 76 additions & 0 deletions src/server/http/__tests__/version_check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import expect from 'expect.js';
import { fromNode } from 'bluebird';
import { resolve } from 'path';
import * as kbnTestServer from '../../../../test/utils/kbn_server';

const src = resolve.bind(null, __dirname, '../../../../src');

const versionHeader = 'kbn-version';
const version = require(src('../package.json')).version;

describe('version_check request filter', function () {
function makeRequest(kbnServer, opts) {
return fromNode(cb => {
kbnTestServer.makeRequest(kbnServer, opts, (resp) => {
cb(null, resp);
});
});
}

async function makeServer() {
const kbnServer = kbnTestServer.createServer();

await kbnServer.ready();

kbnServer.server.route({
path: '/version_check/test/route',
method: 'GET',
handler: function (req, reply) {
reply(null, 'ok');
}
});

return kbnServer;
};

let kbnServer;
beforeEach(async () => kbnServer = await makeServer());
afterEach(async () => await kbnServer.close());

it('accepts requests with the correct version passed in the version header', async function () {
const resp = await makeRequest(kbnServer, {
url: '/version_check/test/route',
method: 'GET',
headers: {
[versionHeader]: version,
},
});

expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});

it('rejects requests with an incorrect version passed in the version header', async function () {
const resp = await makeRequest(kbnServer, {
url: '/version_check/test/route',
method: 'GET',
headers: {
[versionHeader]: `invalid:${version}`,
},
});

expect(resp.statusCode).to.be(400);
expect(resp.headers).to.have.property(versionHeader, version);
expect(resp.payload).to.match(/"Browser client is out of date/);
});

it('accepts requests that do not include a version header', async function () {
const resp = await makeRequest(kbnServer, {
url: '/version_check/test/route',
method: 'GET'
});

expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});
});
40 changes: 21 additions & 19 deletions src/server/http/__tests__/xsrf.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ const nonDestructiveMethods = ['GET', 'HEAD'];
const destructiveMethods = ['POST', 'PUT', 'DELETE'];
const src = resolve.bind(null, __dirname, '../../../../src');

const xsrfHeader = 'kbn-version';
const version = require(src('../package.json')).version;
const xsrfHeader = 'kbn-xsrf';
const versionHeader = 'kbn-version';
const actualVersion = require(src('../package.json')).version;

describe('xsrf request filter', function () {
function inject(kbnServer, opts) {
Expand Down Expand Up @@ -57,59 +58,60 @@ describe('xsrf request filter', function () {
else expect(resp.payload).to.be('ok');
});

it('failes on invalid tokens', async function () {
it('accepts requests with the xsrf header', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
[xsrfHeader]: `invalid:${version}`,
[xsrfHeader]: 'anything',
},
});

expect(resp.statusCode).to.be(400);
expect(resp.headers).to.have.property(xsrfHeader, version);
expect(resp.statusCode).to.be(200);
if (method === 'HEAD') expect(resp.payload).to.be.empty();
else expect(resp.payload).to.match(/"Browser client is out of date/);
else expect(resp.payload).to.be('ok');
});
});
}

for (const method of destructiveMethods) {
context(`destructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func
it('accepts requests with the correct token', async function () {
it('accepts requests with the xsrf header', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
[xsrfHeader]: version,
[xsrfHeader]: 'anything',
},
});

expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});

it('rejects requests without a token', async function () {
// this is still valid for existing csrf protection support
// it does not actually do any validation on the version value itself
it('accepts requests with the version header', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method
method: method,
headers: {
[versionHeader]: actualVersion,
},
});

expect(resp.statusCode).to.be(400);
expect(resp.payload).to.match(/"Missing kbn-version header/);
expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});

it('rejects requests with an invalid token', async function () {
it('rejects requests without either an xsrf or version header', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
[xsrfHeader]: `invalid:${version}`,
},
method: method
});

expect(resp.statusCode).to.be(400);
expect(resp.payload).to.match(/"Browser client is out of date/);
expect(resp.payload).to.match(/"Request must contain an kbn-xsrf header/);
});
});
}
Expand Down
4 changes: 4 additions & 0 deletions src/server/http/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import fs from 'fs';
import Boom from 'boom';
import Hapi from 'hapi';
import getDefaultRoute from './get_default_route';
import versionCheckMixin from './version_check';

module.exports = async function (kbnServer, server, config) {


Expand Down Expand Up @@ -132,5 +134,7 @@ module.exports = async function (kbnServer, server, config) {
}
});

kbnServer.mixin(versionCheckMixin);

return kbnServer.mixin(require('./xsrf'));
};
19 changes: 19 additions & 0 deletions src/server/http/version_check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { badRequest } from 'boom';

export default function (kbnServer, server, config) {
const versionHeader = 'kbn-version';
const actualVersion = config.get('pkg.version');

server.ext('onPostAuth', function (req, reply) {
const versionRequested = req.headers[versionHeader];

if (versionRequested && versionRequested !== actualVersion) {
return reply(badRequest('Browser client is out of date, please refresh the page', {
expected: actualVersion,
got: versionRequested
}));
}

return reply.continue();
});
}
22 changes: 11 additions & 11 deletions src/server/http/xsrf.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { badRequest } from 'boom';

export default function (kbnServer, server, config) {
const version = config.get('pkg.version');
const disabled = config.get('server.xsrf.disableProtection');
const header = 'kbn-version';
const versionHeader = 'kbn-version';
const xsrfHeader = 'kbn-xsrf';

server.ext('onPostAuth', function (req, reply) {
const noHeaderGet = (req.method === 'get' || req.method === 'head') && !req.headers[header];
if (disabled || noHeaderGet) return reply.continue();
if (disabled) {
return reply.continue();
}

const isSafeMethod = req.method === 'get' || req.method === 'head';
const hasVersionHeader = versionHeader in req.headers;
const hasXsrfHeader = xsrfHeader in req.headers;

const submission = req.headers[header];
if (!submission) return reply(badRequest(`Missing ${header} header`));
if (submission !== version) {
return reply(badRequest('Browser client is out of date, please refresh the page', {
expected: version,
got: submission
}));
if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) {
return reply(badRequest(`Request must contain an ${xsrfHeader} header`));
}

return reply.continue();
Expand Down

0 comments on commit ec3abde

Please sign in to comment.