Skip to content

Commit

Permalink
Merge pull request #5587 from spalger/implement/kbnVersionHeader
Browse files Browse the repository at this point in the history
[server/xsrf] use the current version as the xsrf header
  • Loading branch information
spalger committed Dec 9, 2015
2 parents b004690 + fadadb8 commit 9fe99e5
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 131 deletions.
4 changes: 0 additions & 4 deletions config/kibana.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
# The host to bind the server to.
# server.host: "0.0.0.0"

# A value to use as a XSRF token. This token is sent back to the server on each request
# and required if you want to execute requests from other clients (like curl).
# server.xsrf.token: ""

# If you are running kibana behind a proxy, and want to mount it at a path,
# specify that path here. The basePath can't end in a slash.
# server.basePath: ""
Expand Down
1 change: 0 additions & 1 deletion src/server/config/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
152 changes: 61 additions & 91 deletions src/server/http/__tests__/xsrf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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 },
Expand All @@ -41,108 +44,75 @@ 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.payload).to.contain(`"xsrfToken":"${token}"`);
});
});
expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});

context('without configured token', function () {
let kbnServer;
beforeEach(async () => kbnServer = await makeServer());
afterEach(async () => await kbnServer.close());
it('failes on invalid tokens', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
[xsrfHeader]: `invalid:${version}`,
},
});

it('responds with a random token', async function () {
var resp = await inject(kbnServer, {
method: 'GET',
url: '/app/kibana',
expect(resp.statusCode).to.be(400);
expect(resp.headers).to.have.property(xsrfHeader, version);
expect(resp.payload).to.match(/"Browser client is out of date/);
});

expect(resp.payload).to.match(/"xsrfToken":".{64}"/);
});
});

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: {
[xsrfHeader]: version,
},
});

expect(resp.statusCode).to.be(200);
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 () {
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
});

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/);
});

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: {
[xsrfHeader]: `invalid:${version}`,
},
});

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"/);
});
expect(resp.statusCode).to.be(400);
expect(resp.payload).to.match(/"Browser client is out of date/);
});
}
});
});
}
});
8 changes: 4 additions & 4 deletions src/server/http/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,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();
Expand Down
23 changes: 13 additions & 10 deletions src/server/http/xsrf.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
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('Browser client is out of date, please refresh the page', {
expected: version,
got: submission
}));
}

return reply.continue();
});
Expand Down
1 change: 0 additions & 1 deletion src/ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,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, {
Expand Down
29 changes: 14 additions & 15 deletions src/ui/public/chrome/api/__tests__/xsrf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
]);
});

Expand All @@ -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);
});

Expand All @@ -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');
Expand Down Expand Up @@ -113,10 +112,10 @@ describe('chrome xsrf apis', function () {
$httpBackend.flush();
});

it('accepts alternate tokens to use', function () {
const customToken = `custom:${xsrfToken}`;
it('treats the kbnXsrfToken option as boolean-y', function () {
const customToken = `custom:${version}`;
$httpBackend.expectPOST('/api/test', undefined, function (headers) {
return headers[xsrfHeader] === customToken;
return headers[xsrfHeader] === version;
}).respond(200, '');

$http({
Expand Down
10 changes: 5 additions & 5 deletions src/ui/public/chrome/api/xsrf.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ 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 = true }, originalOptions, jqXHR) {
if (kbnXsrfToken) {
jqXHR.setRequestHeader('kbn-xsrf-token', kbnXsrfToken);
jqXHR.setRequestHeader('kbn-version', internals.version);
}
});

chrome.$setupXsrfRequestInterceptor = function ($httpProvider) {
$httpProvider.interceptors.push(function () {
return {
request: function (opts) {
const { kbnXsrfToken = internals.xsrfToken } = opts;
const { kbnXsrfToken = true } = opts;
if (kbnXsrfToken) {
set(opts, ['headers', 'kbn-xsrf-token'], kbnXsrfToken);
set(opts, ['headers', 'kbn-version'], internals.version);
}
return opts;
}
Expand Down

0 comments on commit 9fe99e5

Please sign in to comment.