Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[server/xsrf] use the current version as the xsrf header #5587

Merged
merged 7 commits into from
Dec 9, 2015
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
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
2 changes: 1 addition & 1 deletion src/ui/public/courier/fetch/request/_error_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ define(function (require) {
});

if (!myHandlers.length) {
notify.fatal(new Error('unhandled error ' + (error.stack || error.message)));
notify.fatal(new Error(`unhandled courier request error: ${ notify.describeError(error) }`));
} else {
myHandlers.forEach(function (handler) {
handler.defer.resolve(error);
Expand Down
Loading