7 августа Facebook представил Pysa — ориентированный на безопасность статический анализатор с открытым исходным кодом, помогающий работать с миллионами строк в Instagram. Раскрыты ограничения, затронуты проектные решения и, конечно, средства, помогающие избегать ложных положительных срабатываний. Показана ситуация, когда Pysa наиболее полезен, и код, в котором анализатор неприменим. Подробности из блога Facebook Engineering под катом.
В прошлом году мы писали о том, как создавали Zoncolan, инструмент статического анализа, который анализирует более 100 миллионов строк кода Hack и помогает инженерам предотвращать тысячи потенциальных проблем безопасности. Успех вдохновил на работу над Pysa — Python Static Analyzer. Анализатор построен на основе Pyre, инструмента Facebook для проверки типов Python. Pysa работает с потоком данных в коде. Анализ потоков данных полезен потому, что часто проблемы безопасности и конфиденциальности моделируются как данные, поступающие туда, где их не должно быть.
Pysa помогает выявлять много типов проблем. Анализатор проверяет, правильно ли код использует определённые внутренние структуры для предотвращения доступа или раскрытия пользовательских данных на основе технических политик конфиденциальности. Кроме того, анализатор обнаруживает распространенные проблемы безопасности веб-приложений, такие как XSS и SQL-инъекции. Как и Zoncolan, новый инструмент помог масштабировать усилия в сфере безопасности приложений на Python. Особенно это касается Instagram.
Pysa в Instagram
Самый большой репозиторий Python в Facebook — это миллионы строк на серверах Instagram. Когда Pysa запускается на предлагаемом разработчиком изменении кода, он предоставляет результаты примерно за час, а не за недели или месяцы, которые могут потребоваться для проверки вручную. Это помогает найти и предотвратить проблему достаточно быстро, чтобы она не проникла в кодовую базу. Результаты проверок поступают напрямую разработчику или инженерам по безопасности, в зависимости от типа проблемы и отношения сигнал/шум в конкретной ситуации.
Pysa и Open Source
Исходный код Pysa и множества определений для поиска проблем открыт, чтобы другие разработчики анализировали код своих проектов. Мы работаем с серверными фреймворками с открытым кодом, такими как Django и Tornado, поэтому с первого запуска внутри Facebook Pysa находит проблемы безопасности в проектах, использующих эти фреймворки. Применение Pysa для фреймворков, покрытия для которых ещё нет, обычно не сложнее добавления нескольких строк конфигурации. Нужно просто сообщить анализатору, откуда данные поступают на сервер.
Pysa применялся для обнаружения таких проблем, как CVE-2019-19775, в проектах Python с открытым кодом. Мы также работали с проектом Zulip и включили Pysa в его кодовую базу.
Как это работает?
Pysa разработан с учетом уроков, извлеченных из Zoncolan. Он применяет те же алгоритмы для выполнения статического анализа и даже делит код с Zoncolan. Как и Zoncolan, Pysa отслеживает потоки данных в программе. Пользователь определяет источники важных данных и приёмники, куда данные приходят. В приложениях безопасности наиболее распространенные виды источников — это точки, где контролируемые пользователем данные поступают в приложение, такие как словарь HttpRequest.GET в Django. Приемники, как правило, гораздо более разнообразны и могут включать выполняющие код API. Например,
eval
или os.open
. Pysa итерационно выполняет раунды анализа для построения сводок, чтобы определить, какие функции возвращают данные из источника, а какие имеют параметры, достигающие приемника. Когда анализатор обнаруживает, что источник в конечном итоге подключается к приемнику, то сообщает о проблеме. Визуализация этого процесса — дерево с проблемой на вершине и источниками и потоками в листьях:Чтобы выполнять межпроцедурный анализ — следовать по потокам данных между вызовами функций — нужно иметь возможность сопоставлять вызовы функций с их реализациями. Для этого нужно использовать всю доступную информацию в коде, включая необязательные статические типы, если они присутствуют. Мы работали Pyre, чтобы разобраться с этой информацией. Хотя Pysa в значительной степени полагается на Pyre и оба инструмента совместно используют один репозиторий, важно отметить, что это отдельные продукты с отдельными приложениями.
Ложные срабатывания
Инженеры по безопасности — основные пользователи Pysa в Facebook. Как и любой инженер, работающий с автоматизированными средствами обнаружения ошибок, мы должны были решить, как бороться с ложными положительными срабатываниями (проблемы нет, есть сигнал) и негативами (проблема есть, нет сигнала).
Дизайн Pysa направлен на то, чтобы избегать пропуска проблем и обнаруживать как можно больше реальных проблем. Однако уменьшение количества ложных сигналов может потребовать компромиссов, которые увеличивают количество бесполезных сигналов. Слишком много ложных положительных срабатываний вызывает усталость от тревоги и риск того, что действительно существующие проблемы будут упущены в шуме. В Pysa есть два инструмента для удаления лишних сигналов: санитайзеры и признаки.
Санитайзер — простой инструмент. Он сообщает анализатору не идти по потоку данных после того, как поток пройдёт через функцию или атрибут. Санитайзеры позволяют закодировать доменные знания о преобразованиях, которые всегда представляют данные в безопасной и конфиденциальной форме.
Признаки работают тоньше: это небольшие фрагменты метаданных, которые Pysa присоединяет к потокам данных по мере отслеживания. В отличие от санитайзеров, признаки не удаляют проблемы из результатов анализа. Признаки и другие метаданные могут применяться для фильтрации результатов после анализа. Фильтры обычно пишутся для конкретной пары источник-приемник, чтобы игнорировать проблемы в случае, когда данные уже обработаны для определённого типа (но не для всех типов) приемника.
Чтобы понять, в каких ситуациях Pysa наиболее полезен, представьте, что для загрузки профиля пользователя работает такой код:
# views/user.py
async def get_profile(request: HttpRequest) -> HttpResponse:
profile = load_profile(request.GET['user_id'])
...
# controller/user.py
async def load_profile(user_id: str):
user = load_user(user_id) # Loads a user safely; no SQL injection
pictures = load_pictures(user.id)
...
# model/media.py
async def load_pictures(user_id: str):
query = f"""
SELECT *
FROM pictures
WHERE user_id = {user_id}
"""
result = run_query(query)
...
# model/shared.py
async def run_query(query: str):
connection = create_sql_connection()
result = await connection.execute(query)
...
Здесь потенциальная SQL-инъекция в load_pictures не может эксплуатироваться: эта функция всегда получает валидный
user_id
из функции load_user
в load_profile
. При правильной настройке Pysa, вероятно, не сообщит о проблеме. Теперь представьте, что предприимчивый инженер, который пишет код на уровне контроллера, понял, что одновременная выборка данных пользователя и изображения возвращает результаты быстрее:# controller/user.py
async def load_profile(user_id: str):
user, pictures = await asyncio.gather(
load_user(user_id),
load_pictures(user_id) # no longer 'user.id'!
)
...
Изменение может выглядеть безобидно, но на самом деле заканчивается объединением контролируемой пользователем строки
user_id
с проблемой SQL-инъекции в load_pictures
. В приложении с большим количеством уровней между точкой входа и запросами базы данных инженер может не понять, что данные полностью контролируются пользователем, или что проблема инъекции скрывается в вызываемой функции. Это именно та ситуация, для которой написан анализатор. Когда инженер предлагает подобное изменение в Instagram, Pysa обнаруживает, что данные идут от управляемого пользователем ввода до SQL-запроса, и сообщает о проблеме.Ограничения анализатора
Невозможно написать идеальный статический анализатор. Pysa имеет ограничения, связанные с областью применения, потоком данных и проектными решениями, компромиссом с производительностью ради точности и аккуратности. Python как динамический язык обладает уникальными особенностями, лежащими в основе некоторых из этих проектных решений.
Пространство проблемы
Pysa создан для обнаружения только проблем безопасности, связанных с потоками данных. Не все проблемы безопасности или конфиденциальности моделируются как потоки данных. Посмотрите пример:
def admin_operation(request: HttpRequest):
if not user_is_admin():
return Http404
delete_user(request.GET["user_to_delete"])
Pysa — не тот инструмент, чтобы гарантировать, что проверка авторизации
user_is_admin
запущена перед привилегированной операцией delete_user
. Анализатор может обнаружить данные из request.GET
, направленные в delete_user
, но эти данные никогда не проходят через проверку user_is_admin
. Код можно переписать, чтобы сделать проблему моделируемой Pysa, или встроить проверку разрешений в административную операцию delete_user
. Но этот код прежде всего показывает, какие проблемы не решает Pysa.Ограничения ресурсов
Мы приняли проектное решение об ограничениях, чтобы Pysa мог закончить анализ до того, как предлагаемые разработчиками изменения попадут в кодовую базу. Когда анализатор отслеживает потоки данных в слишком большом количестве атрибутов объекта, иногда приходится упрощать и рассматривать весь объект как именно содержащий эти данные. Это может привести к ложным срабатываниям.
Ещё одно ограничение — время разработки. Оно заставило пойти на компромисс в том, какие особенности Python поддерживаются. Pysa пока не включает декораторы в граф вызовов при вызове функций и поэтому пропускает проблемы внутри декораторов.
Python как динамический язык
Гибкость Python затрудняют статический анализ. Трудно отслеживать потоки данных через вызовы методов без информации о типе. В коде ниже невозможно определить, какая из реализаций
fly
вызывается:class Bird:
def fly(self): ...
class Airplane:
def fly(self): ...
def take_off(x):
x.fly() # Which function does this call?
Анализатор работает в полностью нетипизированных проектах. Но требуется небольшое усилие, чтобы покрыть важные типы.
Динамическая природа Python накладывает ещё одно ограничение. Смотрите ниже:
def secret_eval(request: HttpRequest):
os = importlib.import_module("os")
# Pysa won't know what 'os' is, and thus won't
# catch this remote code execution issue
os.system(request.GET["command"])
Здесь ясно видна уязвимость выполнения, но анализатор пропустит её. Модуль
os
импортируется динамически. Pysa не понимает, что локальная переменная os представляет именно модуль os
. Python позволяет динамически импортировать практически любой код и в любой момент. Кроме того, в языке можно изменять поведение вызова функции практически для любого объекта. Pysa может научиться анализировать os и обнаруживать проблему. Но динамизм Python означает, что существуют бесконечное множество примеров патологических потоков данных, которые анализатор не увидит.Результаты
В первой половине 2020 года на счету Pysa 44 процента всех обнаруженных в Instagram проблем. Среди всех типов уязвимостей в предлагаемых изменениях кода найдены 330 уникальные проблемы. 49 (15%) проблем оказались существенными, 131 из проблем (40%) реальны, но имели смягчающие обстоятельства. Ложные негативы зафиксированы в 150 (45%) случаев.
Мы регулярно рассматриваем проблемы, сообщаемые другими способами. Например, через программу Bug Bounty. Так мы убеждаемся, что исправляем все ложные отрицательные сигналы. Обнаружение каждого типа уязвимости настраивается. Благодаря постоянным уточнениям инженеры по безопасности перешли к более совершенным типам, чтобы сообщать о действительно существующих проблемах в 100 процентах случаев.
В целом мы довольны компромиссами, на которые пошли, чтобы помочь инженерам безопасности масштабироваться. Но всегда есть пространство для развития. Мы создали Pysa, чтобы постоянно повышать качество кода благодаря тесному сотрудничеству инженеров безопасности и программистов. Это позволило нам быстро выполнить итерации и создать инструмент, удовлетворяющий наши потребности лучше, чем любое готовое решение. Сотрудничество инженеров привело к дополнениям и уточнениям в механизмах Pysa. Например, изменился способ просмотра трассировки проблем. Теперь проще видеть ложные негативы.
Документация анализатора Pysa и туториал.
Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя онлайн-курсы SkillFactory:
- Курс «Python для веб-разработки» (9 месяцев)
- Курс «Python для анализа данных» (2 месяца)
- Комплексный курс по глубокому обучению на Python (10 недель)
Eще курсы
- Курс по Machine Learning (12 недель)
- Продвинутый курс «Machine Learning Pro + Deep Learning» (20 недель)
- Курс «Математика и Machine Learning для Data Science» (20 недель)
- Обучение профессии Data Science с нуля (12 месяцев)
- Профессия Веб-разработчик (8 месяцев)
- Профессия аналитика с любым стартовым уровнем (9 месяцев)
- Курс по DevOps (12 месяцев)
- Профессия Java-разработчик с нуля (18 месяцев)
- Курс по JavaScript (12 месяцев)
leon_nikitin
Название инструмента не благозвучно для русской аудитории…
NChechulin
Я читаю как пайса, никаких подтекстов не заметил
osmanpasha
Ну или Пиза; я думаю, авторы делали отсылку к городу
EnotP
Как-то даже не рядом.
ru1z
Тем не менее, в фейсбуке судя по всему говорят «Пиза», youtu.be/8I3zlvtpOww
Про город, вряд-ли отсылка, конечно.
EnotP
Всё может быть. Американцы вообще любители издеваться над фонетикой. Линус Торвальдс неспроста ажно аудио со словом Linux выпускал.
TomskDiver
В документации написано: pronounced like the Leaning Tower of Pisa.
Но соглашусь, разные люди прочитают по разному.