Безопасно ли хранить токен сброса пароля в HTML-форме?

Я хочу добавить функцию сброса / забытого пароля в свое личное приложение express.js. Я решил реализовать его аналогично тому, как это делает Django.

По сути, он генерирует уникальный токен на основе пользователя (идентификатор, хешированный пароль, адрес электронной почты, время последнего входа в систему и текущее время, все вместе с уникальным паролем и солью). Затем пользователь получает этот токен по ссылке для сброса пароля. Кто-то объяснил это лучше меня в одном из ответов stackoverflow.

А вот исходный код класса Django PasswordResetTokenGenerator

Я размещу свою реализацию javascript внизу. Было бы неплохо, если бы вы проверили его на возможные недостатки, но это не мой главный вопрос :)

Итак, пользователь получает электронное письмо со ссылкой для сброса пароля. Ссылка выглядит так https://example.com/reset-password/MQ/58ix7l-35858854f74c35d0c64a5a17bd127f71cd3ad1da, где:

  • MQ - это идентификатор пользователя в кодировке base64 (в этом примере - 1)
  • 58ix7l - метка времени в кодировке base36
  • 35858... - фактический токен

Пользователь нажимает на ссылку. Сервер получает запрос GET - ›сервер проверяет, существует ли пользователь с таким идентификатором -› затем сервер проверяет правильность токена. Если все в порядке, сервер отправляет пользователю html-ответ с формой установки нового пароля.

До сих пор все было почти так же, как это делает django (с небольшими отличиями). Но теперь я хочу поступить иначе. Django (после получения запроса GET) устанавливает анонимный сеанс, сохраняет токен в сеансе и перенаправляет (302) для сброса формы пароля. На стороне клиента нет никаких признаков токена. Пользователь заполняет форму, POST-запрос отправляется на сервер с новым паролем. Сервер снова проверяет токен (хранящийся в сеансе). Если все верно - пароль изменен.

По какой-то причине (это сделает мое приложение намного сложнее :)) я не хочу добавлять анонимный сеанс, я не хочу хранить токен в сеансе.

Я хочу просто взять токен из req.params - ›избежать его -› проверить, действителен ли он - ›и отправить пользователю с формой, например:

<form action="/reset-password" method="POST">
    <label for="new-password">New password</label><input id="new-password" type="password" name="new-password" />
    <label for="repeat-new-password">Repeat new password</label><input id="repeat-new-password" type="password" name="repeat-new-password" />
    <input name="token" type="hidden" value="58ix7l-35858854f74c35d0c64a5a17bd127f71cd3ad1da">
    <input type="submit" value="Set new password" />
</form>

Пользователь отправляет форму, сервер снова проверяет токен, а затем меняет пароль.

Итак, после стены текста мой вопрос:

Насколько безопасно хранить токен в такой HTML-форме?

Я могу придумать одну возможную угрозу: злой пользователь может отправить кому-нибудь ссылку с <script>alert('boo!')</script> вместо токена. Но это не должно быть проблемой, если токен был проверен и экранирован раньше. Какие-нибудь другие возможные дыры?

Как я уже сказал, я публикую свою реализацию javascript generateToken, checkToken, на всякий случай ...


generate-change-password-token.js

const { differenceInSeconds } = require('date-fns');
const makeTokenWithTimestamp = require('../crypto/make-token-with-timestamp');

function generateChangePasswordToken(user) {
    const timestamp = differenceInSeconds(new Date(), new Date(2010, 1, 1));
    const token = makeTokenWithTimestamp(user, timestamp);
    return token;
}

module.exports = generateChangePasswordToken;

verify-change-password-token.js

const crypto = require('crypto');
const { differenceInSeconds } = require('date-fns');
const makeTokenWithTimestamp = require('../crypto/make-token-with-timestamp');

function verifyChangePasswordToken(user, token) {
    const timestamp = parseInt(token.split('-')[0], 36);

    const difference = differenceInSeconds(new Date(), new Date(2010, 1, 1)) - timestamp;

    if (difference > 60 * 60 * 24) {
        return false;
    }
    const newToken = makeTokenWithTimestamp(user, timestamp);
    const valid = crypto.timingSafeEqual(Buffer.from(token), Buffer.from(newToken));
    if (valid === true) {
        return true;
    }
    return false;
}

module.exports = verifyChangePasswordToken;

make-token-with-timestamp.js

const crypto = require('crypto');

function saltedHmac(keySalt, value, secret) {
    const hash = crypto.createHash('sha1').update(keySalt + secret).digest('hex');
    const hmac = crypto.createHmac('sha1', hash).update(value).digest('hex');
    return hmac;
}

function makeHashValue(user, timestamp) {
    const { last_login: lastLogin, id, password } = user;
    const loginTimestamp = lastLogin ? lastLogin.getTime() : '';
    return String(id) + password + String(loginTimestamp) + String(timestamp);
}

function makeTokenWithTimestamp(user, timestamp) {
    const timestamp36 = timestamp.toString(36);
    const hashValue = makeHashValue(user, timestamp);
    const keySalt = process.env.KEY_SALT;
    const secret = process.env.SECRET_KEY;
    if (!(keySalt && secret)) {
        throw new Error('You need to set KEY_SALT and SECRET_KEY in env variables');
    }
    const hashString = saltedHmac(keySalt, hashValue, secret);
    return `${timestamp36}-${hashString}`;
}

module.exports = makeTokenWithTimestamp;

Спасибо


person m51    schedule 13.02.2020    source источник
comment
Если вы собираетесь хранить токен на клиенте, я бы лучше сохранил его как HttpOnly cookie, к которому у javascript нет доступа.   -  person Taplar    schedule 14.02.2020
comment
Пользователь уже может получить токен по ссылке в письме, так что это не опасно. Я предлагаю вам поместить все эти поля в скрытые поля ввода и выполнить такую ​​же проверку при отправке формы.   -  person Barmar    schedule 14.02.2020


Ответы (1)


С точки зрения безопасности нет большой разницы между хранением токена сброса в URL-адресе (переменная get) или в форме (как переменная post). В обоих случаях любой, у кого есть доступ к URL-адресу, получит доступ для сброса пароля.

Как вы упомянули, вам нужно следить за XSS-атаками (встраивать javascript в токен, который затем отображается на странице), и проверка того, что токен является только буквенно-цифровым, должна решить эту конкретную проблему. Вы также захотите остерегаться атак в стиле CORS, с которыми большинство фреймворков может справиться за вас.

Для меня две другие вещи, которые следует учитывать:

  1. Срок действия токена истекает через разумный промежуток времени, так как это, по сути, пароль, который можно использовать для управления учетной записью.

  2. Эти уведомления отправляются после любого запроса пароля, так что, если пользователь намеренно не сбрасывал свой собственный пароль, он мог быть осведомлен об инциденте.

person Robert Hafner    schedule 13.02.2020