Skip to content
This repository was archived by the owner on Mar 31, 2024. It is now read-only.
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: 4 additions & 0 deletions config/kibana.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
9 changes: 6 additions & 3 deletions src/plugins/elasticsearch/lib/__tests__/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ describe('plugins/elasticsearch', function () {

before(function () {
kbnServer = new KbnServer({
server: { autoListen: false },
server: {
autoListen: false,
xsrf: {
disableProtection: true
}
},
logging: { quiet: true },
plugins: {
scanDirs: [
Expand Down Expand Up @@ -104,5 +109,3 @@ describe('plugins/elasticsearch', function () {

});
});


10 changes: 7 additions & 3 deletions src/server/config/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ let path = require('path');

let utils = require('requirefrom')('src/utils');
let fromRoot = utils('fromRoot');
const randomBytes = require('crypto').randomBytes;

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')),
Expand Down Expand Up @@ -39,7 +40,11 @@ module.exports = Joi.object({
origin: ['*://localhost:9876'] // karma test server
}),
otherwise: Joi.boolean().default(false)
})
}),
xsrf: Joi.object({
token: Joi.string().default(randomBytes(32).toString('hex')),
disableProtection: Joi.boolean().default(false),
}).default(),
}).default(),

logging: Joi.object().keys({
Expand Down Expand Up @@ -106,4 +111,3 @@ module.exports = Joi.object({
}).default()

}).default();

2 changes: 1 addition & 1 deletion src/server/config/setup.js
Original file line number Diff line number Diff line change
@@ -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 || {});
};
145 changes: 145 additions & 0 deletions src/server/http/__tests__/xsrf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
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 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 } },
plugins: { scanDirs: [src('plugins')] },
logging: { quiet: true },
optimize: { enabled: false },
});

await kbnServer.ready();

kbnServer.server.route({
path: '/xsrf/test/route',
method: [...nonDestructiveMethods, ...destructiveMethods],
handler: function (req, reply) {
reply(null, 'ok');
}
});

return kbnServer;
}

describe('issuing tokens', function () {
const token = 'secur3';
let kbnServer;
beforeEach(async () => kbnServer = await makeServer(token));
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',
});

expect(resp.payload).to.contain(`"xsrfToken":"${token}"`);
});
});

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 inject(kbnServer, {
method: 'GET',
url: '/app/kibana',
});

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: {
'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
});

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: {
'kbn-xsrf-token': `invalid:${token}`,
},
});

expect(resp.statusCode).to.be(403);
expect(resp.payload).to.match(/"Invalid XSRF token"/);
});
});
}
});
});
2 changes: 2 additions & 0 deletions src/server/http/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,6 @@ module.exports = function (kbnServer, server, config) {
.permanent(true);
}
});

return kbnServer.mixin(require('./xsrf'));
};
20 changes: 20 additions & 0 deletions src/server/http/xsrf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { forbidden } from 'boom';

export default function (kbnServer, server, config) {
const token = config.get('server.xsrf.token');
const disabled = config.get('server.xsrf.disableProtection');

server.decorate('reply', 'issueXsrfToken', function () {
return token;
});

server.ext('onPostAuth', function (req, reply) {
if (disabled || req.method === 'get') 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'));

return reply.continue();
});
}
11 changes: 6 additions & 5 deletions src/server/logging/LogReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
3 changes: 2 additions & 1 deletion src/ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,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, {
Expand Down
Loading