Хеширование паролей в Python: туториал по Bcrypt с примерами
Создание сервера с нуля или использование легковесного фреймворка наделяет огромной властью. С этой властью приходит и ответственность, в частности, ответственность за безопасное хранение паролей пользователей.
Непонимание последствий для безопасности хранения паролей может привести к разрушительным нарушениям и утечкам. Если вы создаете приложение и вам необходимо хранить учетные данные пользователя, изучите хэш-функции.
Могу ли я хранить пароли в виде простого текста?
Чтобы продемонстрировать потенциальную опасность, давайте предположим, что мы не хэшируем пароли на примере фейкового веб-сайта LoveMatchingToday. Скорее всего, когда хакер или недовольный сотрудник получает доступ к базе данных LoveMatchingToday, они загружают все имена пользователей и пароли:
- user.one@gmail.com – somePa$$wordHere
- user.two@hotmail.com – otherPlainTextPass
Теперь злоумышленник может перейти на другие сайты, и, поскольку большинство людей используют одни и те же пароли на разных сайтах, они могут взломать и другие системы.
Наилучшее решение — хеширование
Хеш-функция (или, более конкретно, в нашем случае, функция получения ключа) детерминистически создает надежный ключ из пароля. Поскольку хеши являются односторонними, злоумышленник не может воссоздать незашифрованный пароль из хеша. Теперь злоумышленник увидит что-то подобное в базе данных:
- user.one@gmail.com – cab864878af008fbc550087940ffacdb79a7f82201725e3350e25d6cfbdd4255
- user.two@hotmail.com – 42a7fd2b639d18b3aba5db8504d4530f1f1ab58ab9615414b7629d6ec5c157b8
Они не смогут использовать хеш для входа в другие системы, потому что у них нет доступа к исходному паролю.
В Python существует библиотека Bcrypt, которая является функцией получения сильных ключей, которую можно использовать в производственных системах:
import bcrypt bcrypt.hashpw('userPlainTextPassword'.encode(), bcrypt.gensalt())
Радужные таблицы и соли
В приведенном выше фрагменте кода, вы могли задаться вопросом, что делает функция gensalt(). Если бы мы хешировали пароли без солей, злоумышленник мог бы провести атаку с помощью радужной таблицы, чтобы найти исходный текст.
Соль — это случайная строка данных, хэшированная вместе с паролем, для сохранения уникальности результата хеширования. Соли следует воссоздавать каждый раз, когда новый пароль сохраняется, а соль сохраняется вместе с хешированным результатом, чтобы ее можно было использовать снова для сравнения. Такие библиотеки, как bcrypt, достаточно умны, чтобы хранить соль в результирующей строке, поэтому разработчикам не нужно выполнять дополнительную работу.
Например, допустим, что LoveMatchingToday обновился и начал хешировать пароли, но не содержал уникальных солей. Злоумышленник может иметь предварительно вычисленную таблицу хэшей:
- aab864878af008fbc550087940ffacdb79a7f82201725e3350e25d6cfbdd425f = password123
- afg3683232297323f2f0087940ffacdb79a7f8284723732350e25d6cfbdd4cccc = shadowTheHedgehog1234
Затем он может проверить каждый найденный хэш и посмотреть, совпадает ли хэш с записью в его таблице. Если это так, он может эффективно «развернуть» хэш и получить оригинальный текст.
По этой причине нам нужно «посолить» пароли. К счастью, Bcrypt обрабатывает засолку автоматически. Однако, ради обучения давайте предположим, что этого не случилось. Если бы такого не произошло, наш псевдокод выглядел бы примерно так:
# Save new password salt = creatRandomSalt() hashedPassword = hash(newPassword.concat(salt)) database.save(hashedPassword, salt) # Check password hashedPassword, salt = database.GetUserCredentals() passwordInput = userInput if hash(passwordInput.concat(salt)) == hashedPassword: login() else: failure()
Однако, поскольку Bcrypt автоматически сохраняет соль с хэшированным результатом в формате «», мы можем просто использовать следующий код:
import bcrypt # password = userInput hashAndSalt = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) # save "hashAndSalt" in data base # To check: # password = userInput valid = bcrypt.checkpw(password.encode(), hashAndSalt)
Хранение пароля в Python
Всем привет. Такой вопрос, работаю с Apache Airflow, поднят в контейнерах. Так как стенд тестовый, то часто контейнеры приходится перестраивать (много косячим, приходится все откатывать). Вследствие этого слетают все настроенные соединения, постоянно добавлять их в веб-интерфейсе надоело, появилась задумка сделать баш или питон оператор, который будет добавлять эти соединения. Загвоздка в том, что для подсоединения к БД нужны логин и пароль, а хранить их в скрипте в открытом виде как то не секурно, а в интернете только безопасное хранение полученное от пользователя. Но у меня задача не чтобы при запуске скрипта самому вводить доступы (с таким раскладом я так же могу в веб интерфейсе все это вводить). А как то чтобы настройки соединения сами добавлялись, при этом логин/пароль не хранились в открытом виде в коде
Отслеживать
47.8k 17 17 золотых знаков 56 56 серебряных знаков 100 100 бронзовых знаков
задан 16 мар 2021 в 13:45
Юрий Лифанов Юрий Лифанов
57 7 7 бронзовых знаков
Как не продолбать пароли в Python скриптах
Хранение паролей всегда было головной болью. В классическом варианте у вас есть пользователь, который очень старается не забыть жутко секретный «qwerty123» и информационная система, которая хранит хеш от этого пароля. Хорошая система еще и заботливо солит хеши, чтобы отравить жизнь нехорошим людям, которые могут украсть базу с хешированными паролями. Тут все понятно. Какие-то пароли храним в голове, а какие-то засовываем в зашифрованном виде в keepass.
Все меняется, когда мы убираем из схемы человека, который старательно вводит ключ с бумажки. При взаимодействии двух информационных систем, на клиентской стороне в любом случае должен храниться пароль в открытом для системы виде, чтобы его можно было передать и сравнить с эталонным хешем. И вот на этом этапе админы обычно открывают местный филиал велосипедостроительного завода и начинают старательно прятать, обфусцировать и закапывать секретный ключ в коде скриптов. Многие из этих вариантов не просто бесполезны, но и опасны. Я попробую предложить удобное и безопасное решение этой проблемы для python. И чуть затронем powershell.
Как делать не надо
Всем знакома концепция «временного скриптика». Вот буквально только данные по-быстрому распарсить из базы и удалить. А потом внезапно выясняется, что скрипт уже из dev-зоны мигрировал куда-то в продакшен. И тут начинают всплывать неприятные сюрпризы от изначальной «одноразовости».
Чаще всего встречается вариант в стиле:
db_login = 'john.doe' password = 'password!'
Проблема в том, что здесь пароль светится в открытом виде и достаточно просто обнаруживается среди залежей старых скриптов автоматическим поиском. Чуть более сложный вариант идет по пути security through obscurity, с хранением пароля в зашифрованном виде прямо в коде. При этом расшифровка обратно должна выполняться тут же, иначе клиент не сможет предъявить этот пароль серверной стороне. Такой способ спасет максимум от случайного взгляда, но любой серьезный разбор кода вручную позволит без проблем вытащить секретный ключ. Код ниже спасет только от таких «shoulder surfers»:
- KDE4 & KDE5 KWallet (требуется dbus)
- Freedesktop Secret Service — множество DE, включая GNOME (требуется secretstorage)
- Windows Credential Locker
- macOS Keychain
- Похитить сам код (легко)
- Деобфусцировать при необходимости (легко)
- Похитить сам код (легко)
- Деобфусцировать при необходимости (легко)
- Скомпрометировать локальную машину, залогинившись под атакуемым пользователем (сложно)
Безопасный ввод пароля
Еще один частый вариант утечки секретных паролей — история командной строки. Использование стандартного input здесь недопустимо:
age = input("What is your age? ") print "Your age is: ", age type(age) >>output What is your age? 100 Your age is: 100 type 'int'>
В примере выше я уже упоминал библиотеку getpass:
# Безопасно запрашиваем ввод пароля в CLI password = getpass.getpass(prompt="Enter secret password:")
Ввод данных при ее использовании аналогичен классическому *nix подходу при входе в систему. Ни в какие системные логи данные не пишутся и не отображаются на экране.
Немного о Powershell
Для Powershell правильным вариантом является использование штатного Windows Credential Locker.
Реализуется это модулем CredentialManager.
Install-Module CredentialManager -force New-StoredCredential -Target $url -Username $ENV:Username -Pass . Get-StoredCredential -Target .
Безопасный питон. Осваиваем приемы защищенного кодинга на Python

Сегодня мы поговорим о том, что должно волновать каждого крутого программиста, — о безопасном коде. Ты думаешь, это скучно и сложно? Ничуть! Я поделюсь с тобой своим опытом и покажу, как научиться писать на Python код, за который потом не придется краснеть.
Ограничь область видимости переменных и функций
Область видимости переменной — это контекст, в котором переменная определена и доступна. Если переменная доступна во всем коде, она называется глобальной. Если переменная доступна только внутри функции или метода, она называется локальной.
Смотри, что происходит, если мы используем глобальную переменную:
secret = «my super secret data»
def print_secret () :
# Используем глобальную переменную
print ( secret )
print_secret ( )
Это может быть опасно, потому что глобальные переменные доступны во всем коде и их можно легко изменить. А что, если это важная переменная, которую не следует менять? Злоумышленник может воспользоваться этим и нанести вред.
Поэтому лучше использовать локальные переменные:
def print_secret () :
# Объявляем локальную переменную
secret = «my super secret data»
print ( secret )
print_secret ( )
Теперь переменная secret доступна только внутри функции print_secret( ) . Такой подход не только сделает код более безопасным, но и облегчит его чтение, а также упростит отладку и поддержку.
Разделяй код на модули
Вместо огромного исходника, содержащего описание всех объектов и функций, ты можешь сделать несколько модулей, каждый из которых выполняет свою задачу. Это более выгодный подход, поскольку модули потом будет легко использовать в других проектах.
Но как модульность помогает обезопасить код? Дело в том, что чем меньше кусочки, тем легче в них будет искать ошибки и тем меньше шанс случайно что‑то сломать, когда вносишь изменения. Хорошо организованный код легко менять, и если он разбит на изолированные части, то изменения в одной не затронут другие.
Речь здесь не только о разделении большого проекта на пакеты, которые можно будет импортировать, но и о дроблении кода на функции и объекты.
Вот пример плохого кода:
def do_something () :
# Делаем много разных вещей здесь
# О, и тут мы делаем что-то еще
# И еще что-то здесь
В этом коде все свалено в одну функцию, которая делает множество разных вещей. Это плохо, потому что, если ты найдешь уязвимость в одной из этих вещей, изменения могут повлиять на другие части огромной функции. Чем она больше, тем сложнее будет предсказать результат правок.
А теперь посмотрим на хороший пример:
def do_something_1 () :
# Делаем что-то здесь
def do_something_2 () :
# Делаем что-то здесь
def do_something_3 () :
# Делаем что-то здесь
Мы разбили большую функцию на несколько маленьких, каждая из которых делает что‑то свое. Это гораздо безопаснее, потому что, если мы найдем уязвимость в одной из этих функций, мы сможем ее исправить, не затрагивая остальные. Заодно это делает наш код более читаемым и легким для поддержки, потому что теперь мы знаем, что каждая функция делает только одну вещь.
Еще один хороший способ изолировать код и повторно использовать его — это классы и объекты Python. Классы позволяют нам группировать связанные функции и данные вместе, делая код более управляемым и безопасным.
Вот пример хорошего кода с использованием классов:
class MyAwesomeClass :
def __init__ ( self , some_data ) :
self . some_data = some_data
def do_something_1 ( self ) :
# Делаем что-то с some_data здесь
def do_something_2 ( self ) :
# Делаем что-то еще с some_data здесь
В этом примере мы создаем класс MyAwesomeClass , который содержит два метода: do_something_1 и do_something_2 . Каждый из этих методов работает с данными, которые мы передаем при создании объекта класса. Это позволяет нам контролировать, как эти данные используются и обрабатываются. Безопасность сразу возрастет!
Главный вывод здесь: чем проще и понятнее код и чем легче его поддерживать, тем он безопаснее.
Защитись от инъекций кода
Что такое эти самые инъекции? Представь, что злой пользователь вводит в твое приложение не данные, которые от него запросили, а исполняемый код, который приложение по какой‑то причине возьмет и выполнит. Причем зачастую это не код на Python, а запросы к базе данных на SQL или команды операционной системы. Звучит страшновато? Давай посмотрим, почему такое иногда случается.
Плохой пример
Взгляни на этот кусок кода. Что здесь не так?
def get_user ( name ) :
query = «SELECT * FROM users WHERE name = ‘» + name + «‘»
return execute_query ( query )
Ты просто берешь имя пользователя и сразу втыкаешь его в запрос SQL. А что, если пользователь введет что‑то вроде ‘John’; DROP TABLE users; — ? Поздравляю, ты только что потерял всех своих пользователей! Пример просто классический.
Вот как выглядит безопасная версия этого кода:
def get_user ( name ) :
query = «SELECT * FROM users WHERE name = ?»
return execute_query ( query , ( name ,) )
Здесь мы используем параметризованный запрос, то есть передаем имя пользователя отдельно, и наша база данных гарантированно его экранирует. Это значит, что, даже если пользователь попытается ввести SQL-код, тот будет воспринят просто как строка и ничего плохого не произойдет.
Но это только начало. Всегда помни: ты должен доверять пользовательским данным настолько, насколько доверяешь незнакомцу, вдруг предлагающему тебе сладкую конфету.
Используй безопасные методы сериализации и десериализации
Что за страшные слова — «сериализация» и «десериализация»? Не вызывают ли они дереализацию? Не пугайся! Сериализация — это по сути просто превращение всяких структур вроде списков и словарей в строку, которую легко хранить на диске или передавать по сети. Десериализация — обратный процесс, то есть превращение последовательности символов в структуру.
Здесь кроется целый класс уязвимостей. Если превращать строки в структуры неаккуратно, то злоумышленник, манипулируя данными, сможет перехватить управление твоей программой.
Пример опасного кода:
import pickle
# Никогда так не делай!
def unsafe_deserialization ( serialized_data ) :
return pickle . loads ( serialized_data )
В этом примере я использовал модуль pickle для десериализации данных. Это удобно, но pickle не обеспечивает безопасность. Если злоумышленник подменит сериализованные данные, он сможет выполнить произвольный код на твоем компьютере.
import json
# Гораздо лучше!
def safe_deserialization ( serialized_data ) :
return json . loads ( serialized_data )
Здесь я использую для десериализации модуль json. Он не позволяет выполнить произвольный код, так что он безопаснее. Всегда помни о рисках и выбирай безопасные методы!
info
Уязвимости, вызванные ошибками в десериализации, периодически находят в крупных продуктах. Например, в 2023 году была найдена уязвимость в GoAnywhere MFT. Она позволяла удаленным атакующим выполнять код без аутентификации.
Используй принцип наименьших привилегий
Этот принцип гласит: дай программе только те привилегии, которые ей действительно нужны для выполнения ее задачи.
Это очень важно для безопасности, потому что, если злоумышленник взломает твою программу, он получит те же привилегии, что и программа. Если ее привилегии ограниченны, сузится и круг возможных действий злоумышленника.
Посмотрим на пример. Представь, что у тебя есть функция, которая должна записывать данные в файл:
def write_to_file ( file_path , data ) :
with open ( file_path , ‘w’ ) as f :
f . write ( data )
Этой функции не нужны никакие привилегии, кроме возможности записи в конкретный файл. Но если ты запустишь эту функцию с привилегиями администратора, злоумышленник, взломавший эту функцию, сможет делать в системе абсолютно все.
Всегда давай своим функциям и программам только те привилегии, которые им действительно нужны, и ничего больше.
Избегай уязвимостей, связанных с аутентификацией и авторизацией
Безопасная авторизация пользователей — это огромная тема, в которой есть масса подводных камней. Впрочем, некоторых из них избежать очень легко.
Безопасное хранение паролей
Начнем с того, что абсолютно недопустимо. Никогда (никогда!) не храни пароли в открытом виде. Например, вот так:
«alice» : «password123» ,
«bob» : «qwerty321»
Если эти данные утекут (а вероятность этого всегда есть), то все пароли твоих пользователей станут известны.
Так как же делать правильно? Нужно использовать хеширование паролей. Хеширование — это процесс, при котором из пароля генерируется уникальная строка фиксированной длины. При этом уникальность хеша означает, что даже незначительное изменение в исходном пароле полностью изменит его хеш.
В Python для хеширования можно использовать модуль hashlib. Посмотрим, как это работает, на примере:
import hashlib
password = «password123»
hashed_password = hashlib . sha256 ( password . encode ()) . hexdigest ( )
print ( hashed_password )
Теперь, даже если база данных утечет, хакеры увидят только хеши паролей, а не сами пароли.
Соль для паролей
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
