From 5127086a00c451b682ed0d8e170903e29a8e1c60 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Fri, 25 May 2018 05:44:21 -0700 Subject: [PATCH] [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay --- .../server/lib/crypto/are_tokens_equal.js | 21 +++++++++++++++++++ .../plugins/beats/server/lib/crypto/index.js | 7 +++++++ .../routes/api/register_enroll_beat_route.js | 2 +- .../routes/api/register_update_beat_route.js | 3 ++- 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js create mode 100644 x-pack/plugins/beats/server/lib/crypto/index.js diff --git a/x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js b/x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js new file mode 100644 index 0000000000000..a6ed171d30e5e --- /dev/null +++ b/x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { timingSafeEqual } from 'crypto'; + +const RANDOM_TOKEN_1 = 'b48c4bda384a40cb91c6eb9b8849e77f'; +const RANDOM_TOKEN_2 = '80a3819e3cd64f4399f1d4886be7a08b'; + +export function areTokensEqual(token1, token2) { + if ((typeof token1 !== 'string') || (typeof token2 !== 'string') || (token1.length !== token2.length)) { + // This prevents a more subtle timing attack where we know already the tokens aren't going to + // match but still we don't return fast. Instead we compare two pre-generated random tokens using + // the same comparison algorithm that we would use to compare two equal-length tokens. + return timingSafeEqual(Buffer.from(RANDOM_TOKEN_1, 'utf8'), Buffer.from(RANDOM_TOKEN_2, 'utf8')); + } + + return timingSafeEqual(Buffer.from(token1, 'utf8'), Buffer.from(token2, 'utf8')); +} diff --git a/x-pack/plugins/beats/server/lib/crypto/index.js b/x-pack/plugins/beats/server/lib/crypto/index.js new file mode 100644 index 0000000000000..31fa5de67b2ca --- /dev/null +++ b/x-pack/plugins/beats/server/lib/crypto/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { areTokensEqual } from './are_tokens_equal'; diff --git a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js index 07e336a1e091b..77742c16cd401 100644 --- a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js @@ -80,7 +80,7 @@ export function registerEnrollBeatRoute(server) { try { const enrollmentToken = request.headers['kbn-beats-enrollment-token']; const { token, expires_on: expiresOn } = await getEnrollmentToken(callWithInternalUser, enrollmentToken); - if (!token || token !== enrollmentToken) { + if (!token) { return reply({ message: 'Invalid enrollment token' }).code(400); } if (moment(expiresOn).isBefore(moment())) { diff --git a/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js index fe615ffe1a11c..5955e65f6bbaf 100644 --- a/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js @@ -9,6 +9,7 @@ import { get } from 'lodash'; import { INDEX_NAMES } from '../../../common/constants'; import { callWithInternalUserFactory } from '../../lib/client'; import { wrapEsError } from '../../lib/error_wrappers'; +import { areTokensEqual } from '../../lib/crypto'; async function getBeat(callWithInternalUser, beatId) { const params = { @@ -74,7 +75,7 @@ export function registerUpdateBeatRoute(server) { return reply({ message: 'Beat not found' }).code(404); } - const isAccessTokenValid = beat.access_token === request.headers['kbn-beats-access-token']; + const isAccessTokenValid = areTokensEqual(beat.access_token, request.headers['kbn-beats-access-token']); if (!isAccessTokenValid) { return reply({ message: 'Invalid access token' }).code(401); }