Skip to content
This repository has been archived by the owner on Mar 31, 2024. It is now read-only.

Implement xsrf protection for 4.1 #8

Closed
wants to merge 4 commits into from
Closed
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"simple-git": "^0.11.0",
"sinon": "^1.12.2",
"sinon-as-promised": "^2.0.3",
"supertest": "^1.1.0",
"tar": "^1.0.1"
},
"engines": {
Expand Down
16 changes: 15 additions & 1 deletion src/kibana/plugins/kibana/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,22 @@ define(function (require) {
// ensure that the kibana module requires ui.bootstrap
require('modules')
.get('kibana', ['ui.bootstrap'])
.config(function ($tooltipProvider) {
.config(function ($tooltipProvider, $httpProvider, configFile) {
$tooltipProvider.setTriggers({ 'mouseenter': 'mouseleave click' });
$httpProvider.interceptors.push(function () {
return {
request: function (opts) {
var kbnXsrfToken = configFile.xsrf_token;

if (kbnXsrfToken) {
var headers = opts.headers || (opts.headers = {});
headers['kbn-xsrf-token'] = kbnXsrfToken;
}

return opts;
}
};
});
})
.directive('kibana', function (Private, $rootScope, $injector, Promise, config, kbnSetup) {
return {
Expand Down
2 changes: 2 additions & 0 deletions src/server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var path = require('path');
var favicon = require('serve-favicon');
var requestLogger = require('./lib/requestLogger');
var auth = require('./lib/auth');
var xsrf = require('./lib/xsrf');
var appHeaders = require('./lib/appHeaders');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
Expand All @@ -21,6 +22,7 @@ app.set('x-powered-by', false);

app.use(requestLogger());
app.use(auth());
app.use(xsrf(config.kibana.xsrf_token));
app.use(appHeaders());
app.use(favicon(path.join(config.public_folder, 'styles', 'theme', 'elk.ico')));

Expand Down
2 changes: 2 additions & 0 deletions src/server/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var listPlugins = require('../lib/listPlugins');
var configPath = process.env.CONFIG_PATH || path.join(__dirname, 'kibana.yml');
var kibana = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'));
var env = process.env.NODE_ENV || 'development';
var randomBytes = require('crypto').randomBytes;

function checkPath(path) {
try {
Expand All @@ -22,6 +23,7 @@ kibana.host = kibana.host || '0.0.0.0';
kibana.elasticsearch_url = kibana.elasticsearch_url || 'http://localhost:9200';
kibana.maxSockets = kibana.maxSockets || Infinity;
kibana.log_file = kibana.log_file || null;
kibana.xsrf_token = kibana.xsrf_token || randomBytes(32).toString('hex');

kibana.request_timeout = kibana.startup_timeout == null ? 0 : kibana.request_timeout;
kibana.ping_timeout = kibana.ping_timeout == null ? kibana.request_timeout : kibana.ping_timeout;
Expand Down
4 changes: 4 additions & 0 deletions src/server/config/kibana.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ verify_ssl: true
# If you would like to send the log output to a file you can set the path below.
# This will also turn off the STDOUT log output.
# log_file: ./kibana.log

# 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).
# xsrf_token: ""
16 changes: 16 additions & 0 deletions src/server/lib/xsrf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = function (token) {

function forbidden(res, message) {
res.status(403).json(message);
}

return function (req, res, next) {
if (req.method === 'GET') return next();

var attempt = req.get('kbn-xsrf-token');
if (!attempt) return forbidden(res, 'Missing XSRF token');
if (attempt !== token) return forbidden(res, 'Invalid XSRF token');

return next();
};
};
3 changes: 2 additions & 1 deletion src/server/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ router.get('/config', function (req, res, next) {
var keys = [
'kibana_index',
'default_app_id',
'shard_timeout'
'shard_timeout',
'xsrf_token'
];
var data = _.pick(config.kibana, keys);
data.plugins = config.plugins;
Expand Down
81 changes: 81 additions & 0 deletions test/unit/server/xsrf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
var request = require('supertest');
var expect = require('expect.js');

var app = require('../../../src/server/app');
var router = require('../../../src/server/routes');
var xsrfToken = require('../../../src/server/config').kibana.xsrf_token;

var nonDestructiveMethods = ['get'];
var destructiveMethods = ['post', 'put', 'delete'];

router.all('/xsrf/test/route', function (req, res) {
res.send('ok');
});

describe('xsrf request filter', function () {
describe('issuing tokens', function () {
it('sends a token with /config', function (done) {
request(app)
.get('/config')
.expect(function (res) {
expect(res.body).to.have.property('xsrf_token', xsrfToken);
})
.end(done);
});
});

nonDestructiveMethods.forEach(function (method) {
context('nonDestructiveMethod: ' + method, function () {
var req = function (path) {
return request(app)[method](path);
};

it('accepts requests without a token', function (done) {
req('/xsrf/test/route')
.expect(200)
.end(done);
});

it('ignores invalid tokens', function (done) {
req('/xsrf/test/route')
.set('kbn-xsrf-token', 'invalid:' + xsrfToken)
.expect(200)
.end(done);
});
});
});

destructiveMethods.forEach(function (method) {
context('destructiveMethod: ' + method, function () {
var req = function (path) {
return request(app)[method](path);
};

it('accepts requests with the correct token', function (done) {
req('/xsrf/test/route')
.set('kbn-xsrf-token', xsrfToken)
.expect(200)
.end(done);
});

it('rejects requests without a token', function (done) {
req('/xsrf/test/route')
.expect(403)
.expect(function (resp) {
expect(resp.body).to.be('Missing XSRF token');
})
.end(done);
});

it('rejects requests with an invalid token', function (done) {
req('/xsrf/test/route')
.set('kbn-xsrf-token', 'invalid:' + xsrfToken)
.expect(403)
.expect(function (resp) {
expect(resp.body).to.be('Invalid XSRF token');
})
.end(done);
});
});
});
});
39 changes: 39 additions & 0 deletions test/unit/specs/xsrf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
describe('xsrf protection', function () {
context('Angular support', function () {
var xsrfHeader = 'kbn-xsrf-token';
var xsrfToken;

var $http;
var $httpBackend;

beforeEach(module('kibana'));
beforeEach(inject(function ($injector, configFile) {
$http = $injector.get('$http');
$httpBackend = $injector.get('$httpBackend');

if (!configFile.xsrf_token) {
throw new Error('Config file does not include xsrf_token');
} else {
xsrfToken = configFile.xsrf_token;
}

$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();
});
});
});