diff --git a/config/kibana.yml b/config/kibana.yml index 9f50057cb2f351..7d2401ec48cb78 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -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: "" diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 763ee22db591da..93550a96353644 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -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(), diff --git a/src/server/http/__tests__/xsrf.js b/src/server/http/__tests__/xsrf.js index 39d069c5113c3a..64757a4424dc7c 100644 --- a/src/server/http/__tests__/xsrf.js +++ b/src/server/http/__tests__/xsrf.js @@ -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,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/); }); - } - }); + }); + } }); diff --git a/src/server/http/index.js b/src/server/http/index.js index 22f66ce0a5a3a0..2528ff2e31990c 100644 --- a/src/server/http/index.js +++ b/src/server/http/index.js @@ -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(); diff --git a/src/server/http/xsrf.js b/src/server/http/xsrf.js index 7b205014819211..a3cf8f583d8e5c 100644 --- a/src/server/http/xsrf.js +++ b/src/server/http/xsrf.js @@ -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(); }); diff --git a/src/ui/index.js b/src/ui/index.js index 8f68a018d01caa..905ecd900f0441 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -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, { diff --git a/src/ui/public/chrome/api/__tests__/xsrf.js b/src/ui/public/chrome/api/__tests__/xsrf.js index 9603a0fe35f7a8..326e2f2db84eca 100644 --- a/src/ui/public/chrome/api/__tests__/xsrf.js +++ b/src/ui/public/chrome/api/__tests__/xsrf.js @@ -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'); @@ -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({ diff --git a/src/ui/public/chrome/api/xsrf.js b/src/ui/public/chrome/api/xsrf.js index 244f709a9eaaa6..4d04f9954231de 100644 --- a/src/ui/public/chrome/api/xsrf.js +++ b/src/ui/public/chrome/api/xsrf.js @@ -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 = true }, originalOptions, jqXHR) { if (kbnXsrfToken) { - jqXHR.setRequestHeader('kbn-xsrf-token', kbnXsrfToken); + jqXHR.setRequestHeader('kbn-version', internals.version); } }); @@ -17,9 +17,9 @@ export default function (chrome, internals) { $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; } diff --git a/src/ui/public/courier/fetch/request/_error_handler.js b/src/ui/public/courier/fetch/request/_error_handler.js index 85516b5d04d424..b4f8deba1823d8 100644 --- a/src/ui/public/courier/fetch/request/_error_handler.js +++ b/src/ui/public/courier/fetch/request/_error_handler.js @@ -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); diff --git a/src/ui/public/notify/lib/_format_msg.js b/src/ui/public/notify/lib/_format_msg.js index 87f75e7e37c03a..e0458816836d9e 100644 --- a/src/ui/public/notify/lib/_format_msg.js +++ b/src/ui/public/notify/lib/_format_msg.js @@ -8,7 +8,7 @@ define(function (require) { * @param {String} from - Prefix for message indicating source (optional) * @returns {string} */ - return function formatMsg(err, from) { + function formatMsg(err, from) { var rtn = ''; if (from) { rtn += from + ': '; @@ -21,9 +21,18 @@ define(function (require) { } else if (esMsg) { rtn += esMsg; } else if (err instanceof Error) { - rtn += err.message; + rtn += formatMsg.describeError(err); } return rtn; }; + + formatMsg.describeError = function (err) { + if (!err) return undefined; + if (err.body && err.body.message) return err.body.message; + if (err.message) return err.message; + return '' + err; + }; + + return formatMsg; }); diff --git a/src/ui/public/notify/notifier.js b/src/ui/public/notify/notifier.js index 66203e35f5c2f1..bf9269d683acab 100644 --- a/src/ui/public/notify/notifier.js +++ b/src/ui/public/notify/notifier.js @@ -267,6 +267,8 @@ define(function (require) { }, cb); }; + Notifier.prototype.describeError = formatMsg.describeError; + if (log === _.noop) { Notifier.prototype.log = _.noop; } else {