Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Copy Markdown
Contributor

@spalger spalger Sep 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side quest: I'd love to replace the kbnTestServer.makeRequest() function with something like this. Or update it to use kbnServer.inject() which already promisifies the kbnServer.server.inject().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but not in this PR

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 @@ -9,8 +9,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 @@ -59,59 +60,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
@@ -1,3 +1,5 @@
import versionCheckMixin from './version_check';

module.exports = function (kbnServer, server, config) {
let _ = require('lodash');
let fs = require('fs');
Expand Down Expand Up @@ -185,5 +187,7 @@ module.exports = 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();
});
}
24 changes: 13 additions & 11 deletions src/server/http/xsrf.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
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';
// COMPAT: We continue to check on the kbn-version header for backwards
// compatibility since all existing consumers have been required to use it.
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does xsrf protection care about version headers?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that's the way we handled xsrf protection to date. To stop paying attention to it would mean that anything that generates requests to the server would need to be updated to use the new kbn-xsrf header instead, which isn't a patch release type of change.

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would probably be easier to follow without all the !s

if (isUnsafeMethod && (missingVersionHeader || missingXsrfHeader)) {
  // ...
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here is intentionally that both headers must be missing in order to error. So really it's a question of:

!isSafeMethod && !hasVersionHeader && !hasXsrfHeader
// or
isUnsafeMethod && missingVersionHeader && missingXsrfHeader

I'd rather those negations be applied to boolean values rather than wrapping entire multi-condition statements like req.method === 'get' || req.method === 'head'

return reply(badRequest(`Request must contain an ${xsrfHeader} header`));
}

return reply.continue();
Expand Down