Безопасно ли хранить маркер сброса пароля в html почтовой форме? - PullRequest
0 голосов
/ 14 февраля 2020

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

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

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

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

Итак, пользователь получает электронное письмо со ссылкой для сброса пароля. Ссылка выглядит следующим образом 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> вместо токена. Но это не должно быть проблемой, если токен проверен и экранирован ранее. Любые другие возможные дыры?

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


генерировать -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;

Thx

1 Ответ

1 голос
/ 14 февраля 2020

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

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

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

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

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

...