From 3bcaf5621c6204ddc525bace62c6f2ba95df23b3 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 6 Nov 2015 14:56:24 -0600 Subject: [PATCH 01/24] [config/schema] wrap schema in a function so that we can have random values --- src/server/config/schema.js | 3 +-- src/server/config/setup.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 69b50dc7aaf8..dd3d39a102bd 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -6,7 +6,7 @@ let path = require('path'); let utils = require('requirefrom')('src/utils'); let fromRoot = utils('fromRoot'); -module.exports = Joi.object({ +module.exports = () => Joi.object({ pkg: Joi.object({ version: Joi.string().default(Joi.ref('$version')), buildNum: Joi.number().default(Joi.ref('$buildNum')), @@ -106,4 +106,3 @@ module.exports = Joi.object({ }).default() }).default(); - diff --git a/src/server/config/setup.js b/src/server/config/setup.js index d1d9f8d69b2e..e1728111a2de 100644 --- a/src/server/config/setup.js +++ b/src/server/config/setup.js @@ -1,6 +1,6 @@ module.exports = function (kbnServer) { let Config = require('./Config'); - let schema = require('./schema'); + let schema = require('./schema')(); kbnServer.config = new Config(schema, kbnServer.settings || {}); }; From b65aadf531ea72f186c764e4a0c43750d83e55fc Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 6 Nov 2015 14:57:01 -0600 Subject: [PATCH 02/24] [server] added xsrf protection --- src/server/config/schema.js | 4 +++- src/server/http/index.js | 2 ++ src/server/http/xsrf.js | 24 ++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/server/http/xsrf.js diff --git a/src/server/config/schema.js b/src/server/config/schema.js index dd3d39a102bd..5064f12ffb22 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -5,6 +5,7 @@ let path = require('path'); let utils = require('requirefrom')('src/utils'); let fromRoot = utils('fromRoot'); +const randomBytes = require('crypto').randomBytes; module.exports = () => Joi.object({ pkg: Joi.object({ @@ -39,7 +40,8 @@ module.exports = () => Joi.object({ origin: ['*://localhost:9876'] // karma test server }), otherwise: Joi.boolean().default(false) - }) + }), + xsrfToken: Joi.string().default(randomBytes(256).toString('hex')) }).default(), logging: Joi.object().keys({ diff --git a/src/server/http/index.js b/src/server/http/index.js index 8ee54653fa25..968d84032341 100644 --- a/src/server/http/index.js +++ b/src/server/http/index.js @@ -121,4 +121,6 @@ module.exports = function (kbnServer, server, config) { .permanent(true); } }); + + return kbnServer.mixin(require('./xsrf')); }; diff --git a/src/server/http/xsrf.js b/src/server/http/xsrf.js new file mode 100644 index 000000000000..0f700ddd0f64 --- /dev/null +++ b/src/server/http/xsrf.js @@ -0,0 +1,24 @@ +import { forbidden } from 'boom'; + +export default function (kbnServer, server, config) { + const token = config.get('server.xsrfToken'); + const stateOpts = { + isSecure: config.get('server.ssl.cert') && config.get('server.ssl.key'), + isHttpOnly: false, + path: '/', + }; + + server.ext('onPostAuth', function (req, reply) { + if (req.method === 'get' && !req.state['XSRF-TOKEN'] && !req.headers['x-xsrf-token']) { + reply.state('XSRF-TOKEN', token, stateOpts); + } + + if (req.method === 'get' || req.headers['x-xsrf-token'] === token) { + reply.continue(); + } else if (!req.headers['x-xsrf-token']) { + reply(forbidden('Missing XSRF token')); + } else { + reply(forbidden('Invalid XSRF token')); + } + }); +} From 872672c72f1d56776d0553e4bb477c98d8123747 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 6 Nov 2015 14:57:40 -0600 Subject: [PATCH 03/24] [server] unpipe from the destination, prevent leaking listeners --- src/server/logging/LogReporter.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/server/logging/LogReporter.js b/src/server/logging/LogReporter.js index ac098732a711..e108fde23b82 100644 --- a/src/server/logging/LogReporter.js +++ b/src/server/logging/LogReporter.js @@ -21,12 +21,13 @@ module.exports = class KbnLogger { } init(readstream, emitter, callback) { - readstream - .pipe(this.squeeze) - .pipe(this.format) - .pipe(this.dest); - emitter.on('stop', _.noop); + this.output = readstream.pipe(this.squeeze).pipe(this.format); + this.output.pipe(this.dest); + + emitter.on('stop', () => { + this.output.unpipe(this.dest); + }); callback(); } From 88b80dae15c84a1fe61b290a26a596beaa3db474 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 6 Nov 2015 14:58:06 -0600 Subject: [PATCH 04/24] [server/xsrf] added tests --- src/server/http/__tests__/xsrf.js | 147 ++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/server/http/__tests__/xsrf.js diff --git a/src/server/http/__tests__/xsrf.js b/src/server/http/__tests__/xsrf.js new file mode 100644 index 000000000000..f82f6184eb87 --- /dev/null +++ b/src/server/http/__tests__/xsrf.js @@ -0,0 +1,147 @@ +import expect from 'expect.js'; +import { fromNode as fn } from 'bluebird'; + +import KbnServer from '../../KbnServer'; + +const nonDestructiveMethods = ['GET']; +const destructiveMethods = ['POST', 'PUT', 'DELETE']; + +describe('xsrf request filter', function () { + async function makeServer(token) { + const kbnServer = new KbnServer({ + server: { autoListen: false, xsrfToken: token }, + logging: { quiet: true }, + optimize: { enabled: false }, + }); + + await kbnServer.ready(); + + kbnServer.server.route({ + path: '/csrf/test/route', + method: [...nonDestructiveMethods, ...destructiveMethods], + handler: function (req, reply) { + reply(null, 'ok'); + } + }); + + kbnServer.inject = function (opts) { + return fn(cb => { + kbnServer.server.inject(opts, (resp) => { + cb(null, resp); + }); + }); + }; + + return kbnServer; + } + + context('without configured token', function () { + let kbnServer; + beforeEach(async () => kbnServer = await makeServer()); + afterEach(async () => await kbnServer.close()); + + it('responds with a random token', async function () { + var resp = await kbnServer.inject({ + method: 'GET', + url: '/csrf/test/route', + }); + + expect(resp.headers['set-cookie'][0]).to.match(/^XSRF-TOKEN=[^;]{512}; Path=\/$/); + }); + }); + + 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 and sends it', async function () { + const resp = await kbnServer.inject({ + url: '/csrf/test/route', + method: method + }); + + expect(resp.statusCode).to.be(200); + expect(resp.payload).to.be('ok'); + }); + + it('responds with the token to requests without a token', async function () { + const resp = await kbnServer.inject({ + url: '/csrf/test/route', + method: method + }); + + expect(resp.headers['set-cookie']).to.eql([`XSRF-TOKEN=${token}; Path=/`]); + }); + + it('does not respond with the token to requests with a token', async function () { + const resp = await kbnServer.inject({ + url: '/csrf/test/route', + method: method, + headers: { + 'X-XSRF-TOKEN': token, + }, + }); + + expect(resp.headers).to.not.have.property('set-cookie'); + }); + + it('does not respond with the token to requests that already have token in cookie', async function () { + const resp = await kbnServer.inject({ + url: '/csrf/test/route', + method: method, + headers: { + 'X-XSRF-TOKEN': token, + 'cookie': `XSRF-TOKEN=${token}` + }, + }); + + expect(resp.headers).to.not.have.property('set-cookie'); + }); + }); + } + + for (const method of destructiveMethods) { + context(`destructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func + it('rejects requests without a token', async function () { + const resp = await kbnServer.inject({ + url: '/csrf/test/route', + method: method + }); + + expect(resp.statusCode).to.be(403); + expect(resp.payload).to.match(/"Missing XSRF token"/); + }); + + it('accepts requests with the correct token', async function () { + const resp = await kbnServer.inject({ + url: '/csrf/test/route', + method: method, + headers: { + 'X-XSRF-TOKEN': token, + }, + }); + + expect(resp.statusCode).to.be(200); + expect(resp.payload).to.be('ok'); + }); + + it('rejects requests with an invalid token', async function () { + const resp = await kbnServer.inject({ + url: '/csrf/test/route', + method: method, + headers: { + 'X-XSRF-TOKEN': `invalid:${token}`, + }, + }); + + expect(resp.statusCode).to.be(403); + expect(resp.payload).to.match(/"Invalid XSRF token"/); + }); + }); + } + }); +}); From 470e4c20a6b7b7778050af1f79f653685e2efcbf Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 6 Nov 2015 15:14:57 -0600 Subject: [PATCH 05/24] [server/csrf] allow disabling csrf verification for testing purposes --- .../elasticsearch/lib/__tests__/routes.js | 7 ++++--- src/server/config/schema.js | 6 +++++- src/server/http/xsrf.js | 16 +++++++++++----- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/plugins/elasticsearch/lib/__tests__/routes.js b/src/plugins/elasticsearch/lib/__tests__/routes.js index ca123b108b85..98c71a1c1e53 100644 --- a/src/plugins/elasticsearch/lib/__tests__/routes.js +++ b/src/plugins/elasticsearch/lib/__tests__/routes.js @@ -13,7 +13,10 @@ describe('plugins/elasticsearch', function () { before(function () { kbnServer = new KbnServer({ - server: { autoListen: false }, + server: { + autoListen: false, + xsrfToken: false + }, logging: { quiet: true }, plugins: { scanDirs: [ @@ -104,5 +107,3 @@ describe('plugins/elasticsearch', function () { }); }); - - diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 5064f12ffb22..701ec01b5d51 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -41,7 +41,11 @@ module.exports = () => Joi.object({ }), otherwise: Joi.boolean().default(false) }), - xsrfToken: Joi.string().default(randomBytes(256).toString('hex')) + xsrfToken: Joi + .alternatives() + .try(Joi.string()) + .try(Joi.allow(false)) + .default(randomBytes(256).toString('hex')) }).default(), logging: Joi.object().keys({ diff --git a/src/server/http/xsrf.js b/src/server/http/xsrf.js index 0f700ddd0f64..e44e4f3225fb 100644 --- a/src/server/http/xsrf.js +++ b/src/server/http/xsrf.js @@ -9,16 +9,22 @@ export default function (kbnServer, server, config) { }; server.ext('onPostAuth', function (req, reply) { + if (!token) { + return reply.continue(); + } + if (req.method === 'get' && !req.state['XSRF-TOKEN'] && !req.headers['x-xsrf-token']) { reply.state('XSRF-TOKEN', token, stateOpts); } if (req.method === 'get' || req.headers['x-xsrf-token'] === token) { - reply.continue(); - } else if (!req.headers['x-xsrf-token']) { - reply(forbidden('Missing XSRF token')); - } else { - reply(forbidden('Invalid XSRF token')); + return reply.continue(); } + + if (!req.headers['x-xsrf-token']) { + return reply(forbidden('Missing XSRF token')); + } + + return reply(forbidden('Invalid XSRF token')); }); } From a92beb03742a61d689516543635228f26d1948d1 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 9 Nov 2015 11:23:05 -0600 Subject: [PATCH 06/24] [server/csrf] added test to verify ssl behavior --- src/fixtures/localhost.cert | 18 ++++++++++++++++++ src/fixtures/localhost.key | 27 +++++++++++++++++++++++++++ src/server/http/__tests__/xsrf.js | 26 ++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/fixtures/localhost.cert create mode 100644 src/fixtures/localhost.key diff --git a/src/fixtures/localhost.cert b/src/fixtures/localhost.cert new file mode 100644 index 000000000000..8d8162c22a7f --- /dev/null +++ b/src/fixtures/localhost.cert @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+zCCAeOgAwIBAgIJAMP7l9ufr9h4MA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xNTExMDkxNzE3MzNaFw0yNTExMDYxNzE3MzNaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKaSVv2h6uEsPrUeJDvEsvZJg1oKDBX1eUoFJXNBC0uNGxwO/K3uNEJFtWNq +c0PJfZEY5Sg6Kpy3LdcVS0PZswSSyo1R71Jq1QN4qOHI9CYQD5o4qXz4ChEjy9MC +C8IgK+ntDifVAXgYTtz3O0NOPQlEHzHV+Iwg2VRpl4deqrWozjvvwYpA9a3hgGez +yJLiZDi07MPK93b5t7Ybwliuslu17wYIMUN1SzCfgLuwjAXOo1XX+jeUHw7gtQzi +VB907kan9PZ53ol64znYl7nvbhiSdpLIHC/28SKbbM4t4hcmOigJ0szNKT9c2GdF +Y+74qq0ckrwx08GWdp7lUggjkdMCAwEAAaNQME4wHQYDVR0OBBYEFDo5cgFdxOfQ +UK3yIw+wYi37DfNCMB8GA1UdIwQYMBaAFDo5cgFdxOfQUK3yIw+wYi37DfNCMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAGWeY5GdesdRP5yfbMb+h/Dj +oaEwv4k+r6DSiufh6pnxbXrXmwXZHjwjroDTyrI1Fp5+OJmM67SJi5OtuGBkHoyt +8NE6G5D/hdouSUYL2lPPjRG6la3eYMUU+dGAbxIyVE/lsbWSzNC0soszLNXBZ3Yn +epsef/65H5Ot7uhe0WvrWOg1RmMhmH4zftEizwcNQCEi6LCgkGf2ltel4i1CAhsH +x7aI9KTm/m3RZYy4fm6k+vsI65kWX7vDx1odMdY4Kzf/lJlXC6tKhR9+blDjXdft +ull9PA9iBsI/YWZH6eYo1AlAxsAzylLgyIaoS0BdZ09ET56nX6pwLgSECezdeT8= +-----END CERTIFICATE----- diff --git a/src/fixtures/localhost.key b/src/fixtures/localhost.key new file mode 100644 index 000000000000..78587c1ee82f --- /dev/null +++ b/src/fixtures/localhost.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAppJW/aHq4Sw+tR4kO8Sy9kmDWgoMFfV5SgUlc0ELS40bHA78 +re40QkW1Y2pzQ8l9kRjlKDoqnLct1xVLQ9mzBJLKjVHvUmrVA3io4cj0JhAPmjip +fPgKESPL0wILwiAr6e0OJ9UBeBhO3Pc7Q049CUQfMdX4jCDZVGmXh16qtajOO+/B +ikD1reGAZ7PIkuJkOLTsw8r3dvm3thvCWK6yW7XvBggxQ3VLMJ+Au7CMBc6jVdf6 +N5QfDuC1DOJUH3TuRqf09nneiXrjOdiXue9uGJJ2ksgcL/bxIptszi3iFyY6KAnS +zM0pP1zYZ0Vj7viqrRySvDHTwZZ2nuVSCCOR0wIDAQABAoIBACN3UTJbwWkERK3H +pytarEgoSuFm9j/Orm6GPf0WQlNpzfXhcweNim757K8oQTaTtjqotFImYGBR2F7N +V+MwfR9iKeKBKZXAzW4ZyMuaP/HCxa+ulNfY8DvKBWH+M4a31uHN6Y+tmMx7UH9X +3LRt+iz45jN0PaGIdP22Jd9a1roqyR7VihxH7OD0gph9CN4z8f0GifrHchyKFbV5 +6pxNH87DpPSh+irBMoOFzi0ib4qUUBOm44g7Hcqq6ZgyHPARzTf6ly6IL2ESt5LA +8cbRqpRhmLpJE39+yUU5cfoCImpYoJdMhVfNyzFjdzjokLyoRd1QuFNfYre3cQHl ++g94APECgYEAzpCUprKpMUtagH7OT55NwD/y+qIvY/5s6UyQd+sQJ+IGhPPUxkcd +tZnuaEWuqzjHxN8GWlJbYeiUioy9d0smsYSz+WVe0cpE7dd9iqAiktYnOhplPRAs +FEjTxewRuBTgE3VGwrYgDAtvNY/nh9E+vtJ69bbyhy0cZwIG4kXRyJUCgYEAzm+J +LDNlQfckH3u1MpgaFfS2OP77JHDzgKuF7p7wLbhUF9baUKoEV/g2liaFcXVyG46T +2aosHczdwUjH+XIasrn1LBb4OxDZQ0EabUZawWQGiHYmz45p3yl/mn3KxgOvljTj +VwuAQhfiLU9adDf6rE+hnrTlqLF/S27NaYgWjscCgYEAzf6I/6Rz7eDDpBjRDb1E +tFARs7hBomp7mjzsZWpZdiyFa9jte7439n5HrlyvT7kUH1R6NWCkGQOj/ndUCr87 +GxTHlhJteLFKBBY98By53c0K2XqxMzAJhUELT/mXwgevXjg6FLsjQl+0y6lyr5MQ +C6RDUv7a5csq4961lrkh9/ECgYAaeZl5DrpcxGpgk0gAzhsCV9kK5ECnQskn5leN +69pXsr0uNYLYN4XJFm9BwHz6uRpCSH3Tu4xe4ghKop/q8ORVqZ204tlBEf8bLf1K +qGw5Qy/HTofZtKUFVtgjoyBfVtetBullH3d6gn+iWfv6zbcbZDcRGJgfk2wE65fy +gd6KvwKBgQCR3CJErQ0RwEuvkPoq9pMYtFEVgacl7rs1u4fY0ZivcrdOHbDPUQOb +qKVnL65IOEkaMat+e3KT+977NpAOpIWR7p3f/ubFWTBXKhOjygYjSS+w1uNAFpgT +ClztVlXn0j+S/8Rwy3HeNSl1WHb4CAsaqaPLO1HiDUR5cisNI/avww== +-----END RSA PRIVATE KEY----- diff --git a/src/server/http/__tests__/xsrf.js b/src/server/http/__tests__/xsrf.js index f82f6184eb87..36fdcd1e8651 100644 --- a/src/server/http/__tests__/xsrf.js +++ b/src/server/http/__tests__/xsrf.js @@ -1,15 +1,17 @@ import expect from 'expect.js'; import { fromNode as fn } from 'bluebird'; +import { resolve } from 'path'; import KbnServer from '../../KbnServer'; const nonDestructiveMethods = ['GET']; const destructiveMethods = ['POST', 'PUT', 'DELETE']; +const fromFixture = resolve.bind(null, __dirname, '../../../fixtures/'); describe('xsrf request filter', function () { - async function makeServer(token) { + async function makeServer(token, ssl) { const kbnServer = new KbnServer({ - server: { autoListen: false, xsrfToken: token }, + server: { autoListen: false, xsrfToken: token, ssl: ssl }, logging: { quiet: true }, optimize: { enabled: false }, }); @@ -35,6 +37,26 @@ describe('xsrf request filter', function () { return kbnServer; } + context('with ssl', function () { + let kbnServer; + beforeEach(async () => { + kbnServer = await makeServer(undefined, { + cert: fromFixture('localhost.cert'), + key: fromFixture('localhost.key') + }); + }); + afterEach(async () => await kbnServer.close()); + + it('sets the secure cookie flag', async function () { + var resp = await kbnServer.inject({ + method: 'GET', + url: '/csrf/test/route', + }); + + expect(resp.headers['set-cookie'][0]).to.match(/^XSRF-TOKEN=[^;]{512}; Secure; Path=\/$/); + }); + }); + context('without configured token', function () { let kbnServer; beforeEach(async () => kbnServer = await makeServer()); From 923c558407299bbf7974ac32fc647bb127084cdb Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 9 Nov 2015 11:30:03 -0600 Subject: [PATCH 07/24] [server/csrf] cast isSecure to boolean --- src/server/http/xsrf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/http/xsrf.js b/src/server/http/xsrf.js index e44e4f3225fb..9e48a226ca41 100644 --- a/src/server/http/xsrf.js +++ b/src/server/http/xsrf.js @@ -3,7 +3,7 @@ import { forbidden } from 'boom'; export default function (kbnServer, server, config) { const token = config.get('server.xsrfToken'); const stateOpts = { - isSecure: config.get('server.ssl.cert') && config.get('server.ssl.key'), + isSecure: Boolean(config.get('server.ssl.cert') && config.get('server.ssl.key')), isHttpOnly: false, path: '/', }; From c8e7b829737efcd84a73a9bef40dccb74dba64fd Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 9 Nov 2015 11:31:24 -0600 Subject: [PATCH 08/24] [server/xsrf] require more explicit command to disable xsrf --- src/plugins/elasticsearch/lib/__tests__/routes.js | 4 +++- src/server/config/schema.js | 9 ++++----- src/server/http/__tests__/xsrf.js | 2 +- src/server/http/xsrf.js | 8 ++++---- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/plugins/elasticsearch/lib/__tests__/routes.js b/src/plugins/elasticsearch/lib/__tests__/routes.js index 98c71a1c1e53..204ff1603a4e 100644 --- a/src/plugins/elasticsearch/lib/__tests__/routes.js +++ b/src/plugins/elasticsearch/lib/__tests__/routes.js @@ -15,7 +15,9 @@ describe('plugins/elasticsearch', function () { kbnServer = new KbnServer({ server: { autoListen: false, - xsrfToken: false + xsrf: { + disableProtection: true + } }, logging: { quiet: true }, plugins: { diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 701ec01b5d51..7dd47037918c 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -41,11 +41,10 @@ module.exports = () => Joi.object({ }), otherwise: Joi.boolean().default(false) }), - xsrfToken: Joi - .alternatives() - .try(Joi.string()) - .try(Joi.allow(false)) - .default(randomBytes(256).toString('hex')) + xsrf: Joi.object({ + token: Joi.string().default(randomBytes(256).toString('hex')), + disableProtection: Joi.boolean().default(false), + }).default(), }).default(), logging: Joi.object().keys({ diff --git a/src/server/http/__tests__/xsrf.js b/src/server/http/__tests__/xsrf.js index 36fdcd1e8651..704d34e76a3b 100644 --- a/src/server/http/__tests__/xsrf.js +++ b/src/server/http/__tests__/xsrf.js @@ -11,7 +11,7 @@ const fromFixture = resolve.bind(null, __dirname, '../../../fixtures/'); describe('xsrf request filter', function () { async function makeServer(token, ssl) { const kbnServer = new KbnServer({ - server: { autoListen: false, xsrfToken: token, ssl: ssl }, + server: { autoListen: false, ssl: ssl, xsrf: { token } }, logging: { quiet: true }, optimize: { enabled: false }, }); diff --git a/src/server/http/xsrf.js b/src/server/http/xsrf.js index 9e48a226ca41..a113b2530f0e 100644 --- a/src/server/http/xsrf.js +++ b/src/server/http/xsrf.js @@ -1,7 +1,9 @@ import { forbidden } from 'boom'; export default function (kbnServer, server, config) { - const token = config.get('server.xsrfToken'); + const token = config.get('server.xsrf.token'); + const disabled = config.get('server.xsrf.disableProtection'); + const stateOpts = { isSecure: Boolean(config.get('server.ssl.cert') && config.get('server.ssl.key')), isHttpOnly: false, @@ -9,9 +11,7 @@ export default function (kbnServer, server, config) { }; server.ext('onPostAuth', function (req, reply) { - if (!token) { - return reply.continue(); - } + if (disabled) return reply.continue(); if (req.method === 'get' && !req.state['XSRF-TOKEN'] && !req.headers['x-xsrf-token']) { reply.state('XSRF-TOKEN', token, stateOpts); From 43d8b7383acb48f09751c8bee3c94a3cb183795d Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 9 Nov 2015 11:39:58 -0600 Subject: [PATCH 09/24] [server/xsrf] csrf -> xsrf --- src/server/http/__tests__/xsrf.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/server/http/__tests__/xsrf.js b/src/server/http/__tests__/xsrf.js index 704d34e76a3b..dd7b6af8c4bb 100644 --- a/src/server/http/__tests__/xsrf.js +++ b/src/server/http/__tests__/xsrf.js @@ -19,7 +19,7 @@ describe('xsrf request filter', function () { await kbnServer.ready(); kbnServer.server.route({ - path: '/csrf/test/route', + path: '/xsrf/test/route', method: [...nonDestructiveMethods, ...destructiveMethods], handler: function (req, reply) { reply(null, 'ok'); @@ -50,7 +50,7 @@ describe('xsrf request filter', function () { it('sets the secure cookie flag', async function () { var resp = await kbnServer.inject({ method: 'GET', - url: '/csrf/test/route', + url: '/xsrf/test/route', }); expect(resp.headers['set-cookie'][0]).to.match(/^XSRF-TOKEN=[^;]{512}; Secure; Path=\/$/); @@ -65,7 +65,7 @@ describe('xsrf request filter', function () { it('responds with a random token', async function () { var resp = await kbnServer.inject({ method: 'GET', - url: '/csrf/test/route', + url: '/xsrf/test/route', }); expect(resp.headers['set-cookie'][0]).to.match(/^XSRF-TOKEN=[^;]{512}; Path=\/$/); @@ -82,7 +82,7 @@ describe('xsrf request filter', function () { context(`nonDestructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func it('accepts requests without a token and sends it', async function () { const resp = await kbnServer.inject({ - url: '/csrf/test/route', + url: '/xsrf/test/route', method: method }); @@ -92,7 +92,7 @@ describe('xsrf request filter', function () { it('responds with the token to requests without a token', async function () { const resp = await kbnServer.inject({ - url: '/csrf/test/route', + url: '/xsrf/test/route', method: method }); @@ -101,7 +101,7 @@ describe('xsrf request filter', function () { it('does not respond with the token to requests with a token', async function () { const resp = await kbnServer.inject({ - url: '/csrf/test/route', + url: '/xsrf/test/route', method: method, headers: { 'X-XSRF-TOKEN': token, @@ -113,7 +113,7 @@ describe('xsrf request filter', function () { it('does not respond with the token to requests that already have token in cookie', async function () { const resp = await kbnServer.inject({ - url: '/csrf/test/route', + url: '/xsrf/test/route', method: method, headers: { 'X-XSRF-TOKEN': token, @@ -130,7 +130,7 @@ describe('xsrf request filter', function () { context(`destructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func it('rejects requests without a token', async function () { const resp = await kbnServer.inject({ - url: '/csrf/test/route', + url: '/xsrf/test/route', method: method }); @@ -140,7 +140,7 @@ describe('xsrf request filter', function () { it('accepts requests with the correct token', async function () { const resp = await kbnServer.inject({ - url: '/csrf/test/route', + url: '/xsrf/test/route', method: method, headers: { 'X-XSRF-TOKEN': token, @@ -153,7 +153,7 @@ describe('xsrf request filter', function () { it('rejects requests with an invalid token', async function () { const resp = await kbnServer.inject({ - url: '/csrf/test/route', + url: '/xsrf/test/route', method: method, headers: { 'X-XSRF-TOKEN': `invalid:${token}`, From 28410867f11c41720ffe438c0745c5117193fc0b Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 9 Nov 2015 15:15:14 -0600 Subject: [PATCH 10/24] [server/xsrf] use the new enabled flag --- src/server/http/xsrf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/http/xsrf.js b/src/server/http/xsrf.js index a113b2530f0e..4b281866208d 100644 --- a/src/server/http/xsrf.js +++ b/src/server/http/xsrf.js @@ -5,7 +5,7 @@ export default function (kbnServer, server, config) { const disabled = config.get('server.xsrf.disableProtection'); const stateOpts = { - isSecure: Boolean(config.get('server.ssl.cert') && config.get('server.ssl.key')), + isSecure: config.get('server.ssl.enabled'), isHttpOnly: false, path: '/', }; From 5cbb95295447a7d76da37036f7a75d887a8b200d Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 9 Nov 2015 22:28:41 -0600 Subject: [PATCH 11/24] [server/csrf] update test to match new strategy --- src/fixtures/localhost.cert | 18 ------- src/fixtures/localhost.key | 27 ---------- src/server/http/__tests__/xsrf.js | 86 +++++++++++-------------------- 3 files changed, 31 insertions(+), 100 deletions(-) delete mode 100644 src/fixtures/localhost.cert delete mode 100644 src/fixtures/localhost.key diff --git a/src/fixtures/localhost.cert b/src/fixtures/localhost.cert deleted file mode 100644 index 8d8162c22a7f..000000000000 --- a/src/fixtures/localhost.cert +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIC+zCCAeOgAwIBAgIJAMP7l9ufr9h4MA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV -BAMMCWxvY2FsaG9zdDAeFw0xNTExMDkxNzE3MzNaFw0yNTExMDYxNzE3MzNaMBQx -EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAKaSVv2h6uEsPrUeJDvEsvZJg1oKDBX1eUoFJXNBC0uNGxwO/K3uNEJFtWNq -c0PJfZEY5Sg6Kpy3LdcVS0PZswSSyo1R71Jq1QN4qOHI9CYQD5o4qXz4ChEjy9MC -C8IgK+ntDifVAXgYTtz3O0NOPQlEHzHV+Iwg2VRpl4deqrWozjvvwYpA9a3hgGez -yJLiZDi07MPK93b5t7Ybwliuslu17wYIMUN1SzCfgLuwjAXOo1XX+jeUHw7gtQzi -VB907kan9PZ53ol64znYl7nvbhiSdpLIHC/28SKbbM4t4hcmOigJ0szNKT9c2GdF -Y+74qq0ckrwx08GWdp7lUggjkdMCAwEAAaNQME4wHQYDVR0OBBYEFDo5cgFdxOfQ -UK3yIw+wYi37DfNCMB8GA1UdIwQYMBaAFDo5cgFdxOfQUK3yIw+wYi37DfNCMAwG -A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAGWeY5GdesdRP5yfbMb+h/Dj -oaEwv4k+r6DSiufh6pnxbXrXmwXZHjwjroDTyrI1Fp5+OJmM67SJi5OtuGBkHoyt -8NE6G5D/hdouSUYL2lPPjRG6la3eYMUU+dGAbxIyVE/lsbWSzNC0soszLNXBZ3Yn -epsef/65H5Ot7uhe0WvrWOg1RmMhmH4zftEizwcNQCEi6LCgkGf2ltel4i1CAhsH -x7aI9KTm/m3RZYy4fm6k+vsI65kWX7vDx1odMdY4Kzf/lJlXC6tKhR9+blDjXdft -ull9PA9iBsI/YWZH6eYo1AlAxsAzylLgyIaoS0BdZ09ET56nX6pwLgSECezdeT8= ------END CERTIFICATE----- diff --git a/src/fixtures/localhost.key b/src/fixtures/localhost.key deleted file mode 100644 index 78587c1ee82f..000000000000 --- a/src/fixtures/localhost.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAppJW/aHq4Sw+tR4kO8Sy9kmDWgoMFfV5SgUlc0ELS40bHA78 -re40QkW1Y2pzQ8l9kRjlKDoqnLct1xVLQ9mzBJLKjVHvUmrVA3io4cj0JhAPmjip -fPgKESPL0wILwiAr6e0OJ9UBeBhO3Pc7Q049CUQfMdX4jCDZVGmXh16qtajOO+/B -ikD1reGAZ7PIkuJkOLTsw8r3dvm3thvCWK6yW7XvBggxQ3VLMJ+Au7CMBc6jVdf6 -N5QfDuC1DOJUH3TuRqf09nneiXrjOdiXue9uGJJ2ksgcL/bxIptszi3iFyY6KAnS -zM0pP1zYZ0Vj7viqrRySvDHTwZZ2nuVSCCOR0wIDAQABAoIBACN3UTJbwWkERK3H -pytarEgoSuFm9j/Orm6GPf0WQlNpzfXhcweNim757K8oQTaTtjqotFImYGBR2F7N -V+MwfR9iKeKBKZXAzW4ZyMuaP/HCxa+ulNfY8DvKBWH+M4a31uHN6Y+tmMx7UH9X -3LRt+iz45jN0PaGIdP22Jd9a1roqyR7VihxH7OD0gph9CN4z8f0GifrHchyKFbV5 -6pxNH87DpPSh+irBMoOFzi0ib4qUUBOm44g7Hcqq6ZgyHPARzTf6ly6IL2ESt5LA -8cbRqpRhmLpJE39+yUU5cfoCImpYoJdMhVfNyzFjdzjokLyoRd1QuFNfYre3cQHl -+g94APECgYEAzpCUprKpMUtagH7OT55NwD/y+qIvY/5s6UyQd+sQJ+IGhPPUxkcd -tZnuaEWuqzjHxN8GWlJbYeiUioy9d0smsYSz+WVe0cpE7dd9iqAiktYnOhplPRAs -FEjTxewRuBTgE3VGwrYgDAtvNY/nh9E+vtJ69bbyhy0cZwIG4kXRyJUCgYEAzm+J -LDNlQfckH3u1MpgaFfS2OP77JHDzgKuF7p7wLbhUF9baUKoEV/g2liaFcXVyG46T -2aosHczdwUjH+XIasrn1LBb4OxDZQ0EabUZawWQGiHYmz45p3yl/mn3KxgOvljTj -VwuAQhfiLU9adDf6rE+hnrTlqLF/S27NaYgWjscCgYEAzf6I/6Rz7eDDpBjRDb1E -tFARs7hBomp7mjzsZWpZdiyFa9jte7439n5HrlyvT7kUH1R6NWCkGQOj/ndUCr87 -GxTHlhJteLFKBBY98By53c0K2XqxMzAJhUELT/mXwgevXjg6FLsjQl+0y6lyr5MQ -C6RDUv7a5csq4961lrkh9/ECgYAaeZl5DrpcxGpgk0gAzhsCV9kK5ECnQskn5leN -69pXsr0uNYLYN4XJFm9BwHz6uRpCSH3Tu4xe4ghKop/q8ORVqZ204tlBEf8bLf1K -qGw5Qy/HTofZtKUFVtgjoyBfVtetBullH3d6gn+iWfv6zbcbZDcRGJgfk2wE65fy -gd6KvwKBgQCR3CJErQ0RwEuvkPoq9pMYtFEVgacl7rs1u4fY0ZivcrdOHbDPUQOb -qKVnL65IOEkaMat+e3KT+977NpAOpIWR7p3f/ubFWTBXKhOjygYjSS+w1uNAFpgT -ClztVlXn0j+S/8Rwy3HeNSl1WHb4CAsaqaPLO1HiDUR5cisNI/avww== ------END RSA PRIVATE KEY----- diff --git a/src/server/http/__tests__/xsrf.js b/src/server/http/__tests__/xsrf.js index dd7b6af8c4bb..3cdd4f1ed779 100644 --- a/src/server/http/__tests__/xsrf.js +++ b/src/server/http/__tests__/xsrf.js @@ -6,12 +6,13 @@ import KbnServer from '../../KbnServer'; const nonDestructiveMethods = ['GET']; const destructiveMethods = ['POST', 'PUT', 'DELETE']; -const fromFixture = resolve.bind(null, __dirname, '../../../fixtures/'); +const src = resolve.bind(null, __dirname, '../../../../src'); describe('xsrf request filter', function () { - async function makeServer(token, ssl) { + async function makeServer(token) { const kbnServer = new KbnServer({ - server: { autoListen: false, ssl: ssl, xsrf: { token } }, + server: { autoListen: false, xsrf: { token } }, + plugins: { scanDirs: [src('plugins')] }, logging: { quiet: true }, optimize: { enabled: false }, }); @@ -37,23 +38,19 @@ describe('xsrf request filter', function () { return kbnServer; } - context('with ssl', function () { + describe('issuing tokens', function () { + const token = 'secur3'; let kbnServer; - beforeEach(async () => { - kbnServer = await makeServer(undefined, { - cert: fromFixture('localhost.cert'), - key: fromFixture('localhost.key') - }); - }); + beforeEach(async () => kbnServer = await makeServer(token)); afterEach(async () => await kbnServer.close()); - it('sets the secure cookie flag', async function () { + it('sends a token when rendering an app', async function () { var resp = await kbnServer.inject({ method: 'GET', - url: '/xsrf/test/route', + url: '/app/kibana', }); - expect(resp.headers['set-cookie'][0]).to.match(/^XSRF-TOKEN=[^;]{512}; Secure; Path=\/$/); + expect(resp.payload).to.contain(`"xsrfToken":"${token}"`); }); }); @@ -65,10 +62,10 @@ describe('xsrf request filter', function () { it('responds with a random token', async function () { var resp = await kbnServer.inject({ method: 'GET', - url: '/xsrf/test/route', + url: '/app/kibana', }); - expect(resp.headers['set-cookie'][0]).to.match(/^XSRF-TOKEN=[^;]{512}; Path=\/$/); + expect(resp.payload).to.match(/"xsrfToken":".{512}"/); }); }); @@ -80,7 +77,7 @@ describe('xsrf request filter', function () { for (const method of nonDestructiveMethods) { context(`nonDestructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func - it('accepts requests without a token and sends it', async function () { + it('accepts requests without a token', async function () { const resp = await kbnServer.inject({ url: '/xsrf/test/route', method: method @@ -90,44 +87,36 @@ describe('xsrf request filter', function () { expect(resp.payload).to.be('ok'); }); - it('responds with the token to requests without a token', async function () { - const resp = await kbnServer.inject({ - url: '/xsrf/test/route', - method: method - }); - - expect(resp.headers['set-cookie']).to.eql([`XSRF-TOKEN=${token}; Path=/`]); - }); - - it('does not respond with the token to requests with a token', async function () { + it('ignores invalid tokens', async function () { const resp = await kbnServer.inject({ url: '/xsrf/test/route', method: method, headers: { - 'X-XSRF-TOKEN': token, + 'kbn-xsrf-token': `invalid:${token}`, }, }); - expect(resp.headers).to.not.have.property('set-cookie'); - }); - - it('does not respond with the token to requests that already have token in cookie', async function () { - const resp = await kbnServer.inject({ - url: '/xsrf/test/route', - method: method, - headers: { - 'X-XSRF-TOKEN': token, - 'cookie': `XSRF-TOKEN=${token}` - }, - }); - - expect(resp.headers).to.not.have.property('set-cookie'); + 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 kbnServer.inject({ + 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 kbnServer.inject({ url: '/xsrf/test/route', @@ -138,25 +127,12 @@ describe('xsrf request filter', function () { expect(resp.payload).to.match(/"Missing XSRF token"/); }); - it('accepts requests with the correct token', async function () { - const resp = await kbnServer.inject({ - url: '/xsrf/test/route', - method: method, - headers: { - 'X-XSRF-TOKEN': token, - }, - }); - - expect(resp.statusCode).to.be(200); - expect(resp.payload).to.be('ok'); - }); - it('rejects requests with an invalid token', async function () { const resp = await kbnServer.inject({ url: '/xsrf/test/route', method: method, headers: { - 'X-XSRF-TOKEN': `invalid:${token}`, + 'kbn-xsrf-token': `invalid:${token}`, }, }); From 8c1a709a07353cc0237ff767205cc9a9c7b1e766 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 9 Nov 2015 22:29:44 -0600 Subject: [PATCH 12/24] [xsrf] issue tokens via chrome vars and inject manually --- src/server/http/xsrf.js | 26 ++++++++------------------ src/ui/index.js | 3 ++- src/ui/public/chrome/api/angular.js | 1 + src/ui/public/chrome/api/xsrf.js | 28 ++++++++++++++++++++++++++++ src/ui/public/chrome/chrome.js | 2 ++ 5 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 src/ui/public/chrome/api/xsrf.js diff --git a/src/server/http/xsrf.js b/src/server/http/xsrf.js index 4b281866208d..7b2050148192 100644 --- a/src/server/http/xsrf.js +++ b/src/server/http/xsrf.js @@ -4,27 +4,17 @@ export default function (kbnServer, server, config) { const token = config.get('server.xsrf.token'); const disabled = config.get('server.xsrf.disableProtection'); - const stateOpts = { - isSecure: config.get('server.ssl.enabled'), - isHttpOnly: false, - path: '/', - }; + server.decorate('reply', 'issueXsrfToken', function () { + return token; + }); server.ext('onPostAuth', function (req, reply) { - if (disabled) return reply.continue(); + if (disabled || req.method === 'get') return reply.continue(); - if (req.method === 'get' && !req.state['XSRF-TOKEN'] && !req.headers['x-xsrf-token']) { - reply.state('XSRF-TOKEN', token, stateOpts); - } + const attempt = req.headers['kbn-xsrf-token']; + if (!attempt) return reply(forbidden('Missing XSRF token')); + if (attempt !== token) return reply(forbidden('Invalid XSRF token')); - if (req.method === 'get' || req.headers['x-xsrf-token'] === token) { - return reply.continue(); - } - - if (!req.headers['x-xsrf-token']) { - return reply(forbidden('Missing XSRF token')); - } - - return reply(forbidden('Invalid XSRF token')); + return reply.continue(); }); } diff --git a/src/ui/index.js b/src/ui/index.js index f2ded3b539c2..8b75e7f519d4 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -66,13 +66,14 @@ module.exports = async (kbnServer, server, config) => { } server.decorate('reply', 'renderApp', function (app) { - let payload = { + const payload = { app: app, nav: uiExports.apps, version: kbnServer.version, buildNum: config.get('pkg.buildNum'), buildSha: config.get('pkg.buildSha'), vars: defaults(app.getInjectedVars(), defaultInjectedVars), + xsrfToken: this.issueXsrfToken(), }; return this.view(app.templateName, { diff --git a/src/ui/public/chrome/api/angular.js b/src/ui/public/chrome/api/angular.js index db003c08a171..ca0926710743 100644 --- a/src/ui/public/chrome/api/angular.js +++ b/src/ui/public/chrome/api/angular.js @@ -24,6 +24,7 @@ module.exports = function (chrome, internals) { a.href = '/elasticsearch'; return a.href; }())) + .config(chrome.$setupCsrfRequestInterceptor) .directive('kbnChrome', function ($rootScope) { return { template: function ($el) { diff --git a/src/ui/public/chrome/api/xsrf.js b/src/ui/public/chrome/api/xsrf.js new file mode 100644 index 000000000000..d0b1bff9d28d --- /dev/null +++ b/src/ui/public/chrome/api/xsrf.js @@ -0,0 +1,28 @@ +import $ from 'jquery'; +import { set } from 'lodash'; + +export default function (chrome, internals) { + + chrome.getXsrfToken = function () { + return internals.xsrfToken; + }; + + $.ajaxPrefilter(function ({ kbnCsrfToken = internals.xsrfToken }, originalOptions, jqXHR) { + if (kbnCsrfToken) { + jqXHR.setRequestHeader('kbn-xsrf-token', kbnCsrfToken); + } + }); + + chrome.$setupCsrfRequestInterceptor = function ($httpProvider) { + $httpProvider.interceptors.push(function () { + return { + request: function (opts) { + const { kbnCsrfToken = internals.xsrfToken } = opts; + if (kbnCsrfToken) { + return set(opts, ['headers', 'kbn-xsrf-token'], kbnCsrfToken); + } + } + }; + }); + }; +} diff --git a/src/ui/public/chrome/chrome.js b/src/ui/public/chrome/chrome.js index 5a162d79980a..c9190ccc4e78 100644 --- a/src/ui/public/chrome/chrome.js +++ b/src/ui/public/chrome/chrome.js @@ -18,6 +18,7 @@ var internals = _.defaults( rootController: null, rootTemplate: null, showAppsLink: null, + xsrfToken: null, brand: null, nav: [], applicationClasses: [] @@ -30,6 +31,7 @@ $('').attr({ }).appendTo('head'); require('./api/apps')(chrome, internals); +require('./api/xsrf')(chrome, internals); require('./api/nav')(chrome, internals); require('./api/angular')(chrome, internals); require('./api/controls')(chrome, internals); From 6892b4acc916a70c6fcdc860533e1cf4a52f9ac1 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 9 Nov 2015 22:30:16 -0600 Subject: [PATCH 13/24] [chrome/csrf] added tests to verify jQuery and angular injectors --- src/ui/public/chrome/api/__tests__/xsrf.js | 111 +++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/ui/public/chrome/api/__tests__/xsrf.js diff --git a/src/ui/public/chrome/api/__tests__/xsrf.js b/src/ui/public/chrome/api/__tests__/xsrf.js new file mode 100644 index 000000000000..f99fbaad1c6d --- /dev/null +++ b/src/ui/public/chrome/api/__tests__/xsrf.js @@ -0,0 +1,111 @@ +import $ from 'jquery'; +import expect from 'expect.js'; +import { stub } from 'auto-release-sinon'; +import ngMock from 'ngMock'; + +import xsrfChromeApi from '../xsrf'; + +const xsrfHeader = 'kbn-xsrf-header'; +const xsrfToken = 'xsrfToken'; + +describe('chrome xsrf apis', function () { + context('jQuery support', function () { + it('adds a global jQuery prefilter', function () { + stub($, 'ajaxPrefilter'); + xsrfChromeApi({}, {}); + expect($.ajaxPrefilter.callCount).to.be(1); + }); + + context('jQuery prefilter', function () { + let prefilter; + const xsrfToken = 'xsrfToken'; + + beforeEach(function () { + stub($, 'ajaxPrefilter'); + xsrfChromeApi({}, { xsrfToken }); + prefilter = $.ajaxPrefilter.args[0][0]; + }); + + it('sets the kbn-xsrf-token header', function () { + const setHeader = stub(); + prefilter({}, {}, { setRequestHeader: setHeader }); + + expect(setHeader.callCount).to.be(1); + expect(setHeader.args[0]).to.eql([ + xsrfHeader, + xsrfToken + ]); + }); + + it('can be canceled by setting the kbnXsrfToken option', function () { + const setHeader = stub(); + prefilter({}, {}, { setRequestHeader: setHeader }); + + expect(setHeader.callCount).to.be(1); + expect(setHeader.args[0]).to.eql([ + xsrfHeader, + xsrfToken + ]); + }); + }); + + context('Angular support', function () { + + let $http; + let $httpBackend; + + beforeEach(function () { + stub($, 'ajaxPrefilter'); + const chrome = {}; + xsrfChromeApi(chrome, { xsrfToken }); + ngMock.module(chrome.$setupCsrfRequestInterceptor); + }); + + beforeEach(ngMock.inject(function ($injector) { + $http = $injector.get('$http'); + $httpBackend = $injector.get('$httpBackend'); + + $httpBackend + .when('POST', '/api/test') + .respond('ok'); + })); + + afterEach(function () { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('injects a kbn-xsrf-token header on every request', function () { + $httpBackend.expectPOST('/api/test', undefined, function (headers) { + return headers[xsrfHeader] === xsrfToken; + }).respond(200, ''); + + $http.post('/api/test'); + $httpBackend.flush(); + }); + + it('skips requests with the kbnCsrfToken set falsey', function () { + $httpBackend.expectPOST('/api/test', undefined, function (headers) { + return !(xsrfHeader in headers); + }).respond(200, ''); + + $http.post({ + url: '/api/test', + xsrfHeader: 0 + }); + + $http.post({ + url: '/api/test', + xsrfHeader: '' + }); + + $http.post({ + url: '/api/test', + xsrfHeader: false + }); + + $httpBackend.flush(); + }); + }); + }); +}); From 5cdeae5fb47aada2814a0079e156d693da298da7 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 9 Nov 2015 23:06:06 -0600 Subject: [PATCH 14/24] [server/csrf] don't tack helper methods onto kbnServer --- src/server/http/__tests__/xsrf.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/server/http/__tests__/xsrf.js b/src/server/http/__tests__/xsrf.js index 3cdd4f1ed779..8755c53024a4 100644 --- a/src/server/http/__tests__/xsrf.js +++ b/src/server/http/__tests__/xsrf.js @@ -9,6 +9,14 @@ const destructiveMethods = ['POST', 'PUT', 'DELETE']; const src = resolve.bind(null, __dirname, '../../../../src'); describe('xsrf request filter', function () { + function inject(kbnServer, opts) { + return fn(cb => { + kbnServer.server.inject(opts, (resp) => { + cb(null, resp); + }); + }); + } + async function makeServer(token) { const kbnServer = new KbnServer({ server: { autoListen: false, xsrf: { token } }, @@ -27,14 +35,6 @@ describe('xsrf request filter', function () { } }); - kbnServer.inject = function (opts) { - return fn(cb => { - kbnServer.server.inject(opts, (resp) => { - cb(null, resp); - }); - }); - }; - return kbnServer; } @@ -45,7 +45,7 @@ describe('xsrf request filter', function () { afterEach(async () => await kbnServer.close()); it('sends a token when rendering an app', async function () { - var resp = await kbnServer.inject({ + var resp = await inject(kbnServer, { method: 'GET', url: '/app/kibana', }); @@ -60,7 +60,7 @@ describe('xsrf request filter', function () { afterEach(async () => await kbnServer.close()); it('responds with a random token', async function () { - var resp = await kbnServer.inject({ + var resp = await inject(kbnServer, { method: 'GET', url: '/app/kibana', }); @@ -78,7 +78,7 @@ describe('xsrf request filter', function () { 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 kbnServer.inject({ + const resp = await inject(kbnServer, { url: '/xsrf/test/route', method: method }); @@ -88,7 +88,7 @@ describe('xsrf request filter', function () { }); it('ignores invalid tokens', async function () { - const resp = await kbnServer.inject({ + const resp = await inject(kbnServer, { url: '/xsrf/test/route', method: method, headers: { @@ -105,7 +105,7 @@ describe('xsrf request filter', function () { 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 kbnServer.inject({ + const resp = await inject(kbnServer, { url: '/xsrf/test/route', method: method, headers: { @@ -118,7 +118,7 @@ describe('xsrf request filter', function () { }); it('rejects requests without a token', async function () { - const resp = await kbnServer.inject({ + const resp = await inject(kbnServer, { url: '/xsrf/test/route', method: method }); @@ -128,7 +128,7 @@ describe('xsrf request filter', function () { }); it('rejects requests with an invalid token', async function () { - const resp = await kbnServer.inject({ + const resp = await inject(kbnServer, { url: '/xsrf/test/route', method: method, headers: { From b4517cbe5cb036103db7c4032abdd4c8263e91af Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 9 Nov 2015 23:28:27 -0600 Subject: [PATCH 15/24] [chrome/csrf] polish up some tests --- src/ui/public/chrome/api/__tests__/xsrf.js | 45 ++++++++++++++-------- src/ui/public/chrome/api/angular.js | 2 +- src/ui/public/chrome/api/xsrf.js | 15 ++++---- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/ui/public/chrome/api/__tests__/xsrf.js b/src/ui/public/chrome/api/__tests__/xsrf.js index f99fbaad1c6d..64f3f9c80805 100644 --- a/src/ui/public/chrome/api/__tests__/xsrf.js +++ b/src/ui/public/chrome/api/__tests__/xsrf.js @@ -5,7 +5,7 @@ import ngMock from 'ngMock'; import xsrfChromeApi from '../xsrf'; -const xsrfHeader = 'kbn-xsrf-header'; +const xsrfHeader = 'kbn-xsrf-token'; const xsrfToken = 'xsrfToken'; describe('chrome xsrf apis', function () { @@ -39,13 +39,8 @@ describe('chrome xsrf apis', function () { it('can be canceled by setting the kbnXsrfToken option', function () { const setHeader = stub(); - prefilter({}, {}, { setRequestHeader: setHeader }); - - expect(setHeader.callCount).to.be(1); - expect(setHeader.args[0]).to.eql([ - xsrfHeader, - xsrfToken - ]); + prefilter({ kbnXsrfToken: false }, {}, { setRequestHeader: setHeader }); + expect(setHeader.callCount).to.be(0); }); }); @@ -58,7 +53,7 @@ describe('chrome xsrf apis', function () { stub($, 'ajaxPrefilter'); const chrome = {}; xsrfChromeApi(chrome, { xsrfToken }); - ngMock.module(chrome.$setupCsrfRequestInterceptor); + ngMock.module(chrome.$setupXsrfRequestInterceptor); }); beforeEach(ngMock.inject(function ($injector) { @@ -84,24 +79,42 @@ describe('chrome xsrf apis', function () { $httpBackend.flush(); }); - it('skips requests with the kbnCsrfToken set falsey', function () { + it('skips requests with the kbnXsrfToken set falsey', function () { $httpBackend.expectPOST('/api/test', undefined, function (headers) { return !(xsrfHeader in headers); }).respond(200, ''); - $http.post({ + $http({ + method: 'POST', url: '/api/test', - xsrfHeader: 0 + kbnXsrfToken: 0 }); - $http.post({ + $http({ + method: 'POST', url: '/api/test', - xsrfHeader: '' + kbnXsrfToken: '' }); - $http.post({ + $http({ + method: 'POST', url: '/api/test', - xsrfHeader: false + kbnXsrfToken: false + }); + + $httpBackend.flush(); + }); + + it('accepts alternate tokens to use', function () { + const customToken = `custom:${xsrfToken}`; + $httpBackend.expectPOST('/api/test', undefined, function (headers) { + return headers[xsrfHeader] === customToken; + }).respond(200, ''); + + $http({ + method: 'POST', + url: '/api/test', + kbnXsrfToken: customToken }); $httpBackend.flush(); diff --git a/src/ui/public/chrome/api/angular.js b/src/ui/public/chrome/api/angular.js index ca0926710743..14e4b5a33dbc 100644 --- a/src/ui/public/chrome/api/angular.js +++ b/src/ui/public/chrome/api/angular.js @@ -24,7 +24,7 @@ module.exports = function (chrome, internals) { a.href = '/elasticsearch'; return a.href; }())) - .config(chrome.$setupCsrfRequestInterceptor) + .config(chrome.$setupXsrfRequestInterceptor) .directive('kbnChrome', function ($rootScope) { return { template: function ($el) { diff --git a/src/ui/public/chrome/api/xsrf.js b/src/ui/public/chrome/api/xsrf.js index d0b1bff9d28d..244f709a9eaa 100644 --- a/src/ui/public/chrome/api/xsrf.js +++ b/src/ui/public/chrome/api/xsrf.js @@ -7,20 +7,21 @@ export default function (chrome, internals) { return internals.xsrfToken; }; - $.ajaxPrefilter(function ({ kbnCsrfToken = internals.xsrfToken }, originalOptions, jqXHR) { - if (kbnCsrfToken) { - jqXHR.setRequestHeader('kbn-xsrf-token', kbnCsrfToken); + $.ajaxPrefilter(function ({ kbnXsrfToken = internals.xsrfToken }, originalOptions, jqXHR) { + if (kbnXsrfToken) { + jqXHR.setRequestHeader('kbn-xsrf-token', kbnXsrfToken); } }); - chrome.$setupCsrfRequestInterceptor = function ($httpProvider) { + chrome.$setupXsrfRequestInterceptor = function ($httpProvider) { $httpProvider.interceptors.push(function () { return { request: function (opts) { - const { kbnCsrfToken = internals.xsrfToken } = opts; - if (kbnCsrfToken) { - return set(opts, ['headers', 'kbn-xsrf-token'], kbnCsrfToken); + const { kbnXsrfToken = internals.xsrfToken } = opts; + if (kbnXsrfToken) { + set(opts, ['headers', 'kbn-xsrf-token'], kbnXsrfToken); } + return opts; } }; }); From 7eefb183e18827a1a015c305da7aca8a7297c454 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 10 Nov 2015 10:35:09 -0600 Subject: [PATCH 16/24] [server/xsrf] shorten the xsrf-token, 512 character is overkill --- src/server/config/schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 7dd47037918c..6bdf9bae1e28 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -42,7 +42,7 @@ module.exports = () => Joi.object({ otherwise: Joi.boolean().default(false) }), xsrf: Joi.object({ - token: Joi.string().default(randomBytes(256).toString('hex')), + token: Joi.string().default(randomBytes(32).toString('hex')), disableProtection: Joi.boolean().default(false), }).default(), }).default(), From 3966e2ea441da32ab0af0592fe2d4759f6210455 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 10 Nov 2015 10:40:14 -0600 Subject: [PATCH 17/24] [chrome/xsrf] added test for getXsrfToken method --- src/ui/public/chrome/api/__tests__/xsrf.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ui/public/chrome/api/__tests__/xsrf.js b/src/ui/public/chrome/api/__tests__/xsrf.js index 64f3f9c80805..9603a0fe35f7 100644 --- a/src/ui/public/chrome/api/__tests__/xsrf.js +++ b/src/ui/public/chrome/api/__tests__/xsrf.js @@ -9,6 +9,14 @@ const xsrfHeader = 'kbn-xsrf-token'; const xsrfToken = 'xsrfToken'; describe('chrome xsrf apis', function () { + describe('#getXsrfToken()', function () { + it('exposes the token', function () { + const chrome = {}; + xsrfChromeApi(chrome, { xsrfToken }); + expect(chrome.getXsrfToken()).to.be(xsrfToken); + }); + }); + context('jQuery support', function () { it('adds a global jQuery prefilter', function () { stub($, 'ajaxPrefilter'); From 5b38f328cf0b3faa04be6e4464c922673ba88cfd Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 10 Nov 2015 10:58:04 -0600 Subject: [PATCH 18/24] [config] add not about server.xsrf.token config --- config/kibana.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/kibana.yml b/config/kibana.yml index cd5569bbbd8b..63ea8aaff8e6 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -4,6 +4,10 @@ # 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: "" + # The Elasticsearch instance to use for all your queries. # elasticsearch.url: "http://localhost:9200" From a3906cccd624b4a207dbcd71be6b9565063ca7c9 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 10 Nov 2015 14:31:10 -0600 Subject: [PATCH 19/24] [xsrf] update tests to match 7eefb18 --- src/server/http/__tests__/xsrf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/http/__tests__/xsrf.js b/src/server/http/__tests__/xsrf.js index 8755c53024a4..9098582daa46 100644 --- a/src/server/http/__tests__/xsrf.js +++ b/src/server/http/__tests__/xsrf.js @@ -65,7 +65,7 @@ describe('xsrf request filter', function () { url: '/app/kibana', }); - expect(resp.payload).to.match(/"xsrfToken":".{512}"/); + expect(resp.payload).to.match(/"xsrfToken":".{64}"/); }); }); From 52bd5fae9db0f219975b6e1cd092a7f72f64d6da Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 13 Nov 2015 16:12:06 -0600 Subject: [PATCH 20/24] [discover] fix typo --- src/plugins/kibana/public/discover/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/kibana/public/discover/index.html b/src/plugins/kibana/public/discover/index.html index 9111b3ba53aa..ac17f7bf80d4 100644 --- a/src/plugins/kibana/public/discover/index.html +++ b/src/plugins/kibana/public/discover/index.html @@ -93,7 +93,7 @@

No results found

- Unfortunately I could not find any results matching your search. I tried really hard. I looked all over the place and frankly, I just couldn't find anything good. Help me, help you. Here's some ideas: + Unfortunately I could not find any results matching your search. I tried really hard. I looked all over the place and frankly, I just couldn't find anything good. Help me, help you. Here are some ideas:

From 2571a2940655648212c02324336fe2a9aeb821ff Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 16 Nov 2015 14:30:06 -0600 Subject: [PATCH 21/24] [eslint] named async function are broken see babel/eslint#207 --- src/server/http/__tests__/xsrf.js | 4 ++-- src/server/plugins/initialize.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/http/__tests__/xsrf.js b/src/server/http/__tests__/xsrf.js index 9098582daa46..a38ca767edce 100644 --- a/src/server/http/__tests__/xsrf.js +++ b/src/server/http/__tests__/xsrf.js @@ -17,7 +17,7 @@ describe('xsrf request filter', function () { }); } - async function makeServer(token) { + const makeServer = async function (token) { const kbnServer = new KbnServer({ server: { autoListen: false, xsrf: { token } }, plugins: { scanDirs: [src('plugins')] }, @@ -36,7 +36,7 @@ describe('xsrf request filter', function () { }); return kbnServer; - } + }; describe('issuing tokens', function () { const token = 'secur3'; diff --git a/src/server/plugins/initialize.js b/src/server/plugins/initialize.js index 4b3e6481cdf9..0810718f9125 100644 --- a/src/server/plugins/initialize.js +++ b/src/server/plugins/initialize.js @@ -19,7 +19,7 @@ module.exports = async function (kbnServer, server, config) { let path = []; - async function initialize(id) { + const initialize = async function (id) { let plugin = plugins.byId[id]; if (includes(path, id)) { From 810c5f4f25901cdf1b35f5b7e4a8c37d22ef6203 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 16 Nov 2015 16:12:17 -0600 Subject: [PATCH 22/24] [KbnServer] expose an inject method that promisifys server.inject Hapi server's have a great #inject() method that allows you to inject requests into the server and get their response back. This method uses callbacks though, and does not follow the standard callback interface `cb(err, val)`. Defining this method on our server allows us to promisify it and prevent failures which have left me confused two times now. --- src/server/KbnServer.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/server/KbnServer.js b/src/server/KbnServer.js index 525c92473bcf..3bbbca675ae0 100644 --- a/src/server/KbnServer.js +++ b/src/server/KbnServer.js @@ -83,4 +83,18 @@ module.exports = class KbnServer { async close() { await fromNode(cb => this.server.stop(cb)); } + + async inject(opts) { + if (!this.server) await this.ready(); + + return await fromNode(cb => { + try { + this.server.inject(opts, (resp) => { + cb(null, resp); + }); + } catch (err) { + cb(err); + } + }); + } }; From 45cf100083a541da87f31317b9090b29ebd0c791 Mon Sep 17 00:00:00 2001 From: Antonio Bonuccelli Date: Tue, 3 Nov 2015 16:30:02 +0100 Subject: [PATCH 23/24] bug in command to remove change bin/kibana plugin --remove marvel-ui to bin/kibana plugin --remove marvel --- docs/plugins.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins.asciidoc b/docs/plugins.asciidoc index 94c064e54878..7d4627e69442 100644 --- a/docs/plugins.asciidoc +++ b/docs/plugins.asciidoc @@ -77,7 +77,7 @@ Use the `--remove` or `-r` option to remove a plugin, including any configuratio example: [source,shell] -bin/kibana plugin --remove marvel-ui +bin/kibana plugin --remove marvel You can also remove a plugin manually by deleting the plugin's subdirectory under the `installedPlugins` directory. From 15e6e1f1da96b83c12fe097b254bf638883ae633 Mon Sep 17 00:00:00 2001 From: Antonio Bonuccelli Date: Tue, 3 Nov 2015 16:24:58 +0100 Subject: [PATCH 24/24] bug in command to install changed bin/kibana plugin -i elasticsearch/marvel-ui/latest to bin/kibana plugin -i elasticsearch/marvel/latest --- docs/plugins.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins.asciidoc b/docs/plugins.asciidoc index 7d4627e69442..8352a0382240 100644 --- a/docs/plugins.asciidoc +++ b/docs/plugins.asciidoc @@ -16,7 +16,7 @@ bin/kibana plugin --install // You can also use `-i` instead of `--install`, as in the following example: [source,shell] -bin/kibana plugin -i elasticsearch/marvel-ui/latest +bin/kibana plugin -i elasticsearch/marvel/latest Because the organization given is `elasticsearch`, the plugin management tool automatically downloads the plugin from `download.elastic.co`.