При работе с библиотеками хеширования паролей в JavaScript проблема
повреждения кодировки возникает значительно чаще, чем ошибки самого
алгоритма хеширования. Наиболее опасная ситуация — когда пароль
пользователя доходит до функции hash() уже в изменённом
виде.
Даже один неверно интерпретированный байт полностью меняет итоговый хеш:
password123
и
password123�
создают совершенно разные значения.
Особенно критична ситуация для:
Типичный сценарий:
Пользователь вводит пароль:
Привет123!Браузер отправляет строку через HTTP.
Сервер интерпретирует тело запроса не как UTF-8.
Вместо оригинальной строки получается:
Привет123!Библиотека password-hash создаёт хеш уже от
повреждённой строки.
При следующем входе пароль не совпадает.
Библиотека:
password-hash
работает со строками JavaScript, а строки JavaScript внутри движка представлены в UTF-16.
Пример:
const passwordHash = require('password-hash');
const hash = passwordHash.generate('Привет123');
Если строка уже испорчена до передачи в generate(),
библиотека не способна определить это.
Самая распространённая причина — отсутствие charset.
Неправильно:
Content-Type: application/json
Правильно:
Content-Type: application/json; charset=utf-8
Без указания UTF-8 сервер или промежуточный прокси могут интерпретировать данные в другой кодировке.
В старых приложениях Node.js часто используется неверная настройка middleware.
Проблемный пример:
app.use(express.urlencoded());
Корректнее:
app.use(express.urlencoded({
extended: true
}));
И отдельно:
app.use(express.json());
Ошибка возникает при ручной работе с байтами.
Проблемный код:
const data = buffer.toString();
Node.js попытается использовать кодировку по умолчанию.
Безопаснее явно указывать UTF-8:
const data = buffer.toString('utf8');
Символ:
é
может существовать в двух формах:
Один Unicode-символ:
'é'
Буква + модификатор:
'é'
Визуально строки одинаковы, но бинарно различаются.
Проверка:
console.log('é' === 'é');
Результат:
false
Хеши также будут разными.
Перед хешированием рекомендуется нормализовать строки.
Пример:
const normalized = password.normalize('NFC');
Полный вариант:
const passwordHash = require('password-hash');
function createHash(password) {
const normalized = password.normalize('NFC');
return passwordHash.generate(normalized);
}
Некоторые прокси или старые backend-системы преобразуют Unicode:
Было:
{
"password": "Привет"
}
Стало:
{
"password": "\u041f\u0440\u0438\u0432\u0435\u0442"
}
Само по себе это допустимо.
Проблема начинается, когда строка декодируется дважды.
Ошибка:
JSON.parse(JSON.parse(data));
или:
decodeURIComponent(decodedString);
выполняется повторно.
В результате символы ломаются.
При передаче паролей через query string возникает множество проблем.
Пример:
const encoded = encodeURIComponent(password);
Результат:
%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82
Если сервер не выполняет корректное декодирование UTF-8, строка повреждается.
Небезопасный пример:
GET /login?password=Привет123
Проблемы:
Пароли должны передаваться только в теле POST-запроса.
Старые системы Windows могут использовать:
cp1251
вместо:
utf-8
Тогда:
Привет
превращается в:
Ïðèâåò
Для библиотеки хеширования это уже другая строка.
При диагностике полезно смотреть бинарные данные.
Пример:
const buffer = Buffer.from(password, 'utf8');
console.log(buffer);
Результат:
<Buffer d0 9f d1 80 d0 b8 ...>
Пример диагностики:
const original = 'Привет';
const broken = 'Привет';
console.log(Buffer.from(original));
console.log(Buffer.from(broken));
Разница сразу станет заметна.
При загрузке форм браузеры иногда используют разные charset.
Особенно это касается:
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify({
password
})
});
app.use(express.json());
app.post('/login', (req, res) => {
const password = req.body.password.normalize('NFC');
const hash = passwordHash.generate(password);
res.send(hash);
});
Пример проверки:
function isValidUTF8(str) {
try {
return Buffer.from(str, 'utf8').toString('utf8') === str;
} catch {
return false;
}
}
Некоторые прокси:
Особенно часто это происходит в:
Некоторые системы логирования:
console.log(password);
сохраняют данные в ANSI-кодировке.
При последующем анализе создаётся ложное впечатление, что пароль был испорчен ещё до сервера.
Любая библиотека хеширования работает с байтами.
Неважно, используется ли:
Изменение хотя бы одного байта создаёт полностью другой хеш.
Иногда проблема выявляется через длину.
Пример:
console.log(password.length);
Повреждённая UTF-8 строка часто становится длиннее.
Полезный метод:
const hex = Buffer
.from(password, 'utf8')
.toString('hex');
console.log(hex);
Пример:
d09fd180d0b8d0b2d0b5d182
Иногда пароль временно кодируют в Base64 для безопасной транспортировки.
Пример:
const encoded = Buffer
.from(password, 'utf8')
.toString('base64');
Декодирование:
const decoded = Buffer
.from(encoded, 'base64')
.toString('utf8');
Однако это не замена HTTPS и не метод защиты пароля.
Безопасный подход:
function preparePassword(password) {
if (typeof password !== 'string') {
throw new Error('Invalid password type');
}
return password.normalize('NFC');
}
const express = require('express');
const passwordHash = require('password-hash');
const app = express();
app.use(express.json());
function preparePassword(password) {
if (typeof password !== 'string') {
throw new Error('Password must be string');
}
return password.normalize('NFC');
}
app.post('/register', (req, res) => {
try {
const password = preparePassword(req.body.password);
const hash = passwordHash.generate(password);
res.json({
success: true,
hash
});
} catch (err) {
res.status(400).json({
error: err.message
});
}
});
Причина:
Часто связано с:
Наиболее проблемные категории:
Символы:
a-z
A-Z
0-9
имеют одинаковое представление почти во всех популярных кодировках.
Поэтому проблема часто проявляется только у пользователей с неанглийскими паролями.
Пример тестового набора:
const passwords = [
'Привет123',
'密码123',
'пароль?',
'éèêë',
'مرحبا123'
];
Проверка:
passwords.forEach(password => {
const hash = passwordHash.generate(
password.normalize('NFC')
);
console.log(password, hash);
});
Пользователь уверен, что вводит правильный пароль.
Система тоже корректно сравнивает хеши.
Ошибка находится между ними — на уровне передачи и интерпретации байтов.
Из-за этого диагностика может занимать часы или даже дни, особенно в распределённых системах с несколькими proxy, CDN, балансировщиками и микросервисами.