Се­год­ня мы погово­рим о том, что дол­жно вол­новать каж­дого кру­того прог­раммис­та, — о безопас­ном коде. Ты дума­ешь, это скуч­но и слож­но? Ничуть! Я поделюсь с тобой сво­им опы­том и покажу, как научить­ся писать на 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)

Этой фун­кции не нуж­ны никакие при­виле­гии, кро­ме воз­можнос­ти записи в кон­крет­ный файл. Но если ты запус­тишь эту фун­кцию с при­виле­гиями адми­нис­тра­тора, зло­умыш­ленник, взло­мав­ший эту фун­кцию, смо­жет делать в сис­теме абсо­лют­но все.

Всег­да давай сво­им фун­кци­ям и прог­раммам толь­ко те при­виле­гии, которые им дей­стви­тель­но нуж­ны, и ничего боль­ше.

 

Избегай уязвимостей, связанных с аутентификацией и авторизацией

Бе­зопас­ная авто­риза­ция поль­зовате­лей — это огромная тема, в которой есть мас­са под­водных кам­ней. Впро­чем, некото­рых из них избе­жать очень лег­ко.

 

Безопасное хранение паролей

Нач­нем с того, что абсо­лют­но недопус­тимо. Никог­да (никог­да!) не хра­ни пароли в откры­том виде. Нап­ример, вот так:

users = {
"alice": "password123",
"bob": "qwerty321"
}

Ес­ли эти дан­ные уте­кут (а веро­ятность это­го всег­да есть), то все пароли тво­их поль­зовате­лей ста­нут извес­тны.

Так как же делать пра­виль­но? Нуж­но исполь­зовать хеширо­вание паролей. Хеширо­вание — это про­цесс, при котором из пароля генери­рует­ся уни­каль­ная стро­ка фик­сирован­ной дли­ны. При этом уни­каль­ность хеша озна­чает, что даже нез­начитель­ное изме­нение в исходном пароле пол­ностью изме­нит его хеш.

В Python для хеширо­вания мож­но исполь­зовать модуль hashlib. Пос­мотрим, как это работа­ет, на при­мере:

import hashlib
password = "password123"
hashed_password = hashlib.sha256(password.encode()).hexdigest()
print(hashed_password)

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

 

Соль для паролей

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    1 Комментарий
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии