Главная/Статьи/1С-Битрикс: сохранение пароля пользователя (антипаттерн)

1С-Битрикс: сохранение пароля пользователя (антипаттерн)

Разбор опасной практики сохранения паролей в открытом виде. Почему это критическая уязвимость и как правильно решать задачи, требующие доступа к паролю.

ДМ
Дмитрий Мещеряков
📅 3 апреля 2022 г.📖 7 мин чтения

Просьба «а можно сохранять пароли, чтобы мы могли их посмотреть» приходит от заказчиков регулярно, и почти всегда за ней стоит реальная задача: интеграция с системой, которая умеет только логин и пароль, или поддержка, которая хочет помочь клиенту войти. Просто ответить «нельзя» недостаточно — если не предложить рабочую альтернативу, это сделают за вас и хуже.

Разберём, почему хранение паролей в открытом виде — не «плохой стиль», а инцидент безопасности, и чем закрывается каждая из задач, ради которых к нему прибегают.

Предупреждение

⚠️ Важно

Это антипаттерн! Хранение паролей в открытом виде — критическая уязвимость. Эта статья объясняет, почему так делать нельзя, и предлагает безопасные альтернативы.

Как это обычно делают (неправильно)

Иногда разработчики добавляют сохранение пароля в пользовательское поле:

php
<?php
// ❌ НЕ ДЕЛАЙТЕ ТАК!

AddEventHandler('main', 'OnAfterUserLogin', function (&$fields) {
    if ($fields['USER_ID'] > 0) {
        $user = new CUser();
        $user->Update($fields['USER_ID'], [
            'UF_USER_PASS' => $fields['PASSWORD'], // Пароль в открытом виде!
        ]);
    }
});

Почему это критически опасно

1. Нарушение принципов безопасности

Пароли должны храниться только в хэшированном виде. Битрикс использует безопасное хэширование с солью — пароль невозможно восстановить из хэша. Сохранение в открытом виде уничтожает всю эту защиту.

2. Утечка при взломе

При SQL-инъекции или доступе к бэкапу базы данных злоумышленник получит все пароли в открытом виде.

3. Требования регуляторов

Законодательство о защите персональных данных (152-ФЗ в России, GDPR в ЕС) требует принимать адекватные меры защиты, а хранение паролей в открытом виде адекватной мерой не является ни по одному из отраслевых стандартов. Практический аспект здесь важнее формулировок: при разборе инцидента с утечкой этот факт превращает «нас взломали» в «мы не обеспечили защиту», и разница в последствиях существенная.

4. Риск для других сервисов

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

5. Это ломается даже без утечки

Пароль сохраняется в момент входа, а меняется он в другом месте. Через месяц в поле лежит устаревшее значение, а система, которая на него полагается, начинает вести себя непредсказуемо. Половину таких историй обнаруживают не как уязвимость, а как «интеграция перестала работать у части пользователей».

Какие задачи пытаются решить

Обычно хранение паролей нужно для:

  1. Интеграция с внешними системами — авторизация на стороннем сервисе
  2. Синхронизация аккаунтов — создание учётки в другой системе
  3. Восстановление доступа — помощь пользователю без сброса пароля

Правильные решения

Для интеграции: OAuth/JWT токены

Вместо передачи пароля используйте токены:

php
<?php
// Генерация токена для внешней системы
function generateIntegrationToken(int $userId): string
{
    $token = bin2hex(random_bytes(32));
    $expiry = time() + 3600; // 1 час
    
    // Сохраняем хэш токена, не сам токен.
    // Для случайных токенов достаточно быстрого SHA-256: bcrypt намеренно
    // медленный, и на API, где токен проверяется на каждом запросе,
    // это превращается в узкое место без выигрыша в стойкости
    $user = new CUser();
    $user->Update($userId, [
        'UF_API_TOKEN_HASH' => hash('sha256', $token),
        'UF_API_TOKEN_EXPIRY' => $expiry,
    ]);
    
    return $token; // Токен отдаём пользователю один раз
}

// Проверка токена
function validateToken(int $userId, string $token): bool
{
    $user = CUser::GetByID($userId)->Fetch();
    
    if ($user['UF_API_TOKEN_EXPIRY'] < time()) {
        return false; // Токен истёк
    }
    
    return hash_equals((string) $user['UF_API_TOKEN_HASH'], hash('sha256', $token));
}

Для синхронизации: API с токенами

php
<?php
// Создание пользователя во внешней системе через API
function syncUserToExternalSystem(int $userId): void
{
    $user = CUser::GetByID($userId)->Fetch();
    
    // Генерируем временный пароль для внешней системы
    $tempPassword = bin2hex(random_bytes(16));
    
    // Отправляем во внешнюю систему
    $response = httpClient()->post('https://external.com/api/users', [
        'email' => $user['EMAIL'],
        'name' => $user['NAME'],
        'temp_password' => $tempPassword,
        'require_password_change' => true,
    ]);
    
    // Уведомляем пользователя
    sendEmail($user['EMAIL'], 'Ваш аккаунт создан', 
        "Временный пароль: {$tempPassword}\nСмените его при первом входе.");
}

Для SSO: Single Sign-On

Вместо синхронизации паролей внедрите единую точку авторизации:

php
<?php
// Генерация SSO-токена
function generateSsoToken(int $userId): string
{
    $payload = [
        'user_id' => $userId,
        'exp' => time() + 300, // 5 минут
        'nonce' => bin2hex(random_bytes(16)),
    ];
    
    return base64_encode(json_encode($payload)) . '.' . 
           hash_hmac('sha256', json_encode($payload), SSO_SECRET);
}

// Редирект на внешнюю систему с токеном
header('Location: https://external.com/sso?token=' . generateSsoToken($userId));

Для восстановления: ссылка сброса

php
<?php
// Генерация ссылки сброса пароля
function generatePasswordResetLink(int $userId): string
{
    $token = bin2hex(random_bytes(32));
    $expiry = time() + 3600;
    
    $user = new CUser();
    $user->Update($userId, [
        'UF_RESET_TOKEN' => password_hash($token, PASSWORD_DEFAULT),
        'UF_RESET_EXPIRY' => $expiry,
    ]);
    
    return "https://site.ru/reset?user={$userId}&token={$token}";
}

Таблица сравнения подходов

ЗадачаПлохое решениеПравильное решение
Интеграция с APIХранить парольOAuth токены
Синхронизация аккаунтовКопировать парольВременный пароль + смена
SSO между системамиОбщая база паролейJWT/SAML токены
Помощь пользователюПоказать парольСсылка сброса
💡 Совет

Почему для токенов быстрый хэш, а для паролей — медленный. Разница не в «строгости», а в том, что защищает от подбора. Пароль придумал человек: энтропии мало, словарь подходит, поэтому нужен намеренно медленный алгоритм (bcrypt, argon2), делающий перебор невыгодным. Токен из random_bytes(32) перебрать нельзя в принципе — там 256 бит случайности, — поэтому замедлять проверку незачем, и SHA-256 с hash_equals здесь правильный выбор. Использовать bcrypt для токенов, проверяемых на каждом запросе, — это добровольно посаженная себе на API задержка.

Аудит безопасности

Проверьте систему на наличие паролей в открытом виде. Обратите внимание: значения пользовательских полей лежат не в b_user, а в отдельных таблицах — b_uts_user для одиночных значений и b_utm_user для множественных. Запрос вида SELECT UF_* FROM b_user невалиден и ничего не найдёт.

sql
-- 1. Ищем подозрительные пользовательские поля
SELECT ID, ENTITY_ID, FIELD_NAME, USER_TYPE_ID
FROM b_user_field
WHERE ENTITY_ID = 'USER'
  AND (FIELD_NAME LIKE '%PASS%' OR FIELD_NAME LIKE '%PWD%' OR FIELD_NAME LIKE '%SECRET%');

-- 2. Смотрим содержимое найденного поля (подставьте своё имя колонки)
SELECT VALUE_ID, UF_USER_PASS
FROM b_uts_user
WHERE UF_USER_PASS IS NOT NULL AND UF_USER_PASS <> ''
LIMIT 20;
bash
# 3. И то, что чаще всего находится быстрее любого SQL, —
# сам код, который это записывает
grep -rn "PASSWORD" /home/bitrix/www/local/ | grep -iv "CONFIRM_PASSWORD\|checkPassword"

Если нашли — немедленно:

  1. Удалите данные из поля
  2. Принудительно сбросьте пароли затронутых пользователей
  3. Уведомите их о необходимости сменить пароль
  4. Удалите код, сохраняющий пароли

А если пароль действительно нужен

Отдельно про случай, который обычно и приводит людей к этой статье: на проекте есть legacy-система, у которой нет ни API-токенов, ни OAuth, — она принимает только логин и пароль, и ваш сайт должен ходить в неё от имени пользователя.

Честный порядок действий:

  1. Ещё раз проверить, что альтернативы нет. У большинства систем, о которых так говорят, обнаруживается сервисная учётная запись или базовая авторизация на уровне интеграции — то есть один набор учётных данных для сервиса, а не пароль каждого пользователя. Это решает задачу без хранения чужих паролей вообще.
  2. Если пароль всё-таки нужен в восстановимом виде — это уже не хэширование, а шифрование, и разница принципиальна: ключ обязан храниться вне базы данных (переменные окружения, отдельное хранилище секретов), чтобы дамп базы сам по себе ничего не давал. Доступ к расшифровке — из одного места в коде и с логированием каждого обращения.
  3. Зафиксировать это решение письменно — с указанием, кто его принял и почему. Не ради бюрократии: через год про причину забудут, а конструкция останется, и без пояснения её либо повторят там, где не нужно, либо снесут вместе с работающей интеграцией.

Это плохой вариант. Он остаётся плохим, даже когда сделан аккуратно, — но он на порядок лучше поля UF_USER_PASS с паролями в открытом виде, которое появится, если разговор закончится словом «нельзя».

💡 Совет

Итог одной фразой: безопасность — не фича, а требование, и пароли в открытом виде являются инцидентом с момента появления в базе, а не с момента утечки. Но отказ должен приходить вместе с рабочей альтернативой — иначе задачу решат в обход вас.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.