Содержание статьи
- Ограничь область видимости переменных и функций
- Разделяй код на модули
- Защитись от инъекций кода
- Плохой пример
- Используй безопасные методы сериализации и десериализации
- Используй принцип наименьших привилегий
- Избегай уязвимостей, связанных с аутентификацией и авторизацией
- Безопасное хранение паролей
- Соль для паролей
- Тщательно проверяй все входные данные
- Не забивай на управление сессиями
- Используй безопасные куки
- Восстанавливай идентификатор сессии
- Устанавливай тайм-аут сессии
- Будь аккуратен с eval() и exec()
- Используй виртуальное окружение 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';
? Поздравляю, ты только что потерял всех своих пользователей! Пример просто классический.
Вот как выглядит безопасная версия этого кода:
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)
Этой функции не нужны никакие привилегии, кроме возможности записи в конкретный файл. Но если ты запустишь эту функцию с привилегиями администратора, злоумышленник, взломавший эту функцию, сможет делать в системе абсолютно все.
Всегда давай своим функциям и программам только те привилегии, которые им действительно нужны, и ничего больше.
Избегай уязвимостей, связанных с аутентификацией и авторизацией
Безопасная авторизация пользователей — это огромная тема, в которой есть масса подводных камней. Впрочем, некоторых из них избежать очень легко.
Безопасное хранение паролей
Начнем с того, что абсолютно недопустимо. Никогда (никогда!) не храни пароли в открытом виде. Например, вот так:
users = { "alice": "password123", "bob": "qwerty321"}
Если эти данные утекут (а вероятность этого всегда есть), то все пароли твоих пользователей станут известны.
Так как же делать правильно? Нужно использовать хеширование паролей. Хеширование — это процесс, при котором из пароля генерируется уникальная строка фиксированной длины. При этом уникальность хеша означает, что даже незначительное изменение в исходном пароле полностью изменит его хеш.
В Python для хеширования можно использовать модуль hashlib. Посмотрим, как это работает, на примере:
import hashlibpassword = "password123"hashed_password = hashlib.sha256(password.encode()).hexdigest()print(hashed_password)
Теперь, даже если база данных утечет, хакеры увидят только хеши паролей, а не сами пароли.
Соль для паролей
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»