к подходам пересмотра исходного кода относятся
Обзор методов статического анализа исходного кода для поиска уязвимостей
Обзор методов статического анализа исходного кода для поиска уязвимостей
Обзор методов статического анализа исходного кода для поиска уязвимостей
При статическом анализе кода происходит анализ программы без ее реального исполнения, а при динамическом анализе – в процессе исполнения. В большинстве случаев под статическим анализом подразумевают анализ, осуществляемый с помощью автоматизированных инструментов исходного или исполняемого кода.
Исторически первые инструменты статического анализа (часто в их названии используется слово lint) применялись для нахождения простейших дефектов программы. Они использовали простой поиск по сигнатурам, то есть обнаруживали совпадения с имеющимися сигнатурами в базе проверок. Они применяются до сих пор и позволяют определять «подозрительные» конструкции в коде, которые могут вызвать падение программы при выполнении.
Недостатков у такого метода немало. Основным является то, что множество «подозрительных» конструкций в коде не всегда являются дефектами. В большинстве случаев такой код может быть синтаксически правильным и работать корректно. Соотношение «шума» к реальным дефектам может достигать 100:1 на больших проектах. Таким образом, разработчику приходится тратить много времени на его отсеивание от реальных дефектов, что отменяет плюсы автоматизированного поиска.
Несмотря на очевидные недостатки, такие простые утилиты для поиска уязвимостей до сих пор используются. Обычно они распространяются бесплатно, так как коммерческого применения они, по понятным причинам, не получили.
Второе поколение инструментов статического анализа в дополнение к простому поиску совпадений по шаблонам оснащено технологиями анализа, которые до этого применялись в компиляторах для оптимизации программ. Эти методы позволяли по анализу исходного кода составлять графы потока управления и потока данных, которые представляют собой модель выполнения программы и модель зависимостей одних переменных от других. Имея данные, графы можно моделировать, определяя, как будет выполняться программа (по какому пути и с какими данными).
Поскольку программа состоит из множества функций, процедур модулей, которые могут зависеть друг от друга, недостаточно анализировать каждый файл по отдельности. Для полноценного межпроцедурного анализа необходимы все файлы программы и зависимости.
Основным достоинством этого типа анализаторов является меньше количество «шума» за счет частичного моделирования выполнения программ и возможность обнаружения более сложных дефектов.
Процесс поиска уязвимостей в действии
Для иллюстрации приведем процесс поиска уязвимостей инъекции кода и SQL-инъекции (рис. 1).
Для их обнаружения находятся места в программе, откуда поступают недоверенные данные (рис. 2), например, запрос протокола HTTP.
На листинге (рис. 1) 1 на строке 5 данные получаются из HTTP запроса, который поступает от пользователей при запросе Web-страницы. Например, при запросе страницы “http://example.com/main?name =‘ or 1=‘1”. Строка or 1=‘1 попадает в переменную data из объекта request, который содержит HTTP-запрос.
Дальше на строке 10 идет вызов функции Process с аргументом data, которая обрабатывает полученную строку. На строке 12 – конкатенация полученной строки data и запроса к базе данных, уже на строке 15 происходит вызов функции запроса к базе данных c результирующим запросом. В результате данных манипуляции получается запрос к базе данных вида: select * from users where name=‘’ or ‘1’=‘1’.
Что означает выбрать из таблицы всех пользователей, а не пользователя с определенным именем. Это не является стандартным функционалом и влечет нарушение конфиденциальности, что соответственно означает уязвимость. В результате потенциальный злоумышленник может получить информацию о всех пользователях, а не только о конкретном. Также он может получить данные из других таблиц, например содержащих пароли и другие критичные данные. А в некоторых случаях – исполнить свой вредоносный код.
Статические анализаторы работают похожим образом: помечают данные, которые поступают из недоверенного источника, отслеживаются все манипуляции с данными и пытаются определить, попадают ли данные в критичные функции. Под критичными функциями обычно подразумеваются функции, которые исполняют код, делают запросы к БД, обрабатывают XML-документы, осуществляют доступ к файлам и др., в которых изменение параметра функции может нанести ущерб конфиденциальности, целостности и доступности.
Также возможна обратная ситуация, когда из доверенного источника, например переменных окружения, критичных таблиц базы данных, критичных файлов, данные поступают в недоверенный источник, например генерируемую HTML-страницу. Это может означать потенциальную утечку критичной информации.
Одним из недостатков такого анализа является сложность определения на пути выполнения программ функций, которые осуществляют фильтрацию или валидацию значений. Поэтому большинство анализаторов включает набор стандартных системных функций фильтрации для языка и возможность задания таких функций самостоятельно.
Автоматизированный поиск уязвимостей
Достаточно сложно достоверно определить автоматизированными методами наличие закладок в ПО, поскольку необходимо понимать, какие функции выполняет определенный участок программы и являются ли они необходимыми программе, а не внедрены для обхода доступа к ресурсам системы. Но можно найти закладки по определенным признакам (рис. 3). Например, доступ к системе при помощи сравнения данных для авторизации или аутентификации с предопределенными значениями, а не использование стандартных механизмов авторизации или аутентификации. Найти данные признаки можно с помощью простого сигнатурного метода, но анализ потоков данных позволяет более точно определять предопределенные значения в программе, отслеживая, откуда поступило значение, динамически из базы данных или он было «зашито» в программе, что повышает точность анализа.
Нет общего мнения по поводу обязательного функционала третьего поколения инструментов статического анализа. Некоторые вендоры предлагают более тесную интеграцию в процесс разработки, использование SMT-решателей для точного определения пути выполнения программы в зависимости от данных.
Также есть тенденция добавления гибридного анализа, то есть совмещенных функций статического и динамического анализов. У данного подхода есть несомненные плюсы: например, можно проверять существование уязвимости, найденной с помощью статического анализа путем эксплуатации этой уязвимости. Недостатком такого подхода может быть следующая ситуация. В случае ошибочной корреляции места, где не было доказано уязвимостей с помощью динамического анализа, возможно появление ложноотрицательного результата. Другими словами, уязвимость есть, но анализатор ее не находит.
Если говорить о результатах анализа, то для оценки работы статического анализатора используется, как и в статистике, разделение результата анализа на положительный, отрицательный, ложноотрицатель-ный (дефект есть, но анализатор его не находит) и ложнопо-ложительный (дефекта нет, но анализатор его находит).
Для реализации эффективного процесса устранения дефектов важно отношение количества истинно найденных ко всем найденным дефектам. Данное отношение называют точностью. При небольшой точности получается большое соотношение истинных дефектов к ложноположительным, что так же, как и в ситуации с большим количеством шума, требует от разработчиков много времени на анализ результатов и фактически нивелирует плюсы автоматизированного анализа кода.
Для поиска уязвимостей особенно важно отношение найденных истинных уязвимостей ко всем найденным, поскольку данное отношение и принято считать полнотой. Ненайденные уязвимости опаснее ложнопо-ложительного результата, так как могут нести прямой ущерб бизнесу.
Достаточно сложно в одном решении сочетать хорошую полноту и точность анализа. Инструменты первого поколения, работающие по простому совпадению шаблонов, могут показывать хорошую полноту анализа, но при этом низкую точность из-за ограничения технологий. Благодаря тому что второе поколение анализаторов может определять зависимости и пути выполнения программы, обеспечивается более высокая точность анализа при такой же полноте.
Несмотря на то что развитие технологий происходит непрерывно, автоматизированные инструменты до сих пор не заменяют полностью ручной аудит кода. Такие категории дефектов, как логические, архитектурные уязвимости и проблемы с производительностью, могут быть обнаружены только экспертом. Однако инструменты работают быстрее, позволяют автоматизировать процесс и стоят дешевле, чем работа аудитора. При внедрении статического анализа кода можно использовать ручной аудит для первичной оценки, поскольку это позволяет обнаруживать серьезные проблемы с архитектурой. Автоматизированные же инструменты должны применяться для быстрого исправления дефектов. Например, при появлении новой версии ПО.
Существует множество решений для статического анализа исходного кода. Выбор продукта зависит от поставленных задач. Если необходимо повысить качество кода, то вполне можно использовать анализаторы первого поколения, использующие поиск по шаблонам. В случае когда нужно найти уязвимости в ходе реализации цикла безопасной разработки, логично использовать инструменты, использующие анализ потока данных. Ну а если опыт внедрения средств статического и динамического анализа уже имеется, можно попробовать средства, использующие гибридный анализ.
Что такое рефакторинг кода и зачем он нужен
Программу можно написать тысячей разных способов, и она будет работать. Но код придётся почистить.
Рефакторинг — это переработка исходного кода программы, чтобы он стал более простым и понятным.
Рефакторинг не меняет поведение программы, не исправляет ошибки и не добавляет новую функциональность. Он делает код более понятным и удобочитаемым.
Например, вот фрагмент на Python, создающий список из строки:
При рефакторинге его можно упростить, применив конструктор списков:
Результат работы программы не изменился, но код стал проще, компактнее и понятнее.
Последовательность таких небольших изменений может сильно улучшить качество проекта.
Программист, консультант, специалист по документированию. Легко и доступно рассказывает о сложных вещах в программировании и дизайне.
Зачем нужен рефакторинг
Стройный, хорошо структурированный код легко читается и быстро дорабатывается. Но редко удаётся сразу сделать его таким. Разработчики спешат, в процессе могут меняться требования к задаче, тестировщики находят баги, которые нужно быстро исправить, или возникают срочные доработки, и их приходится делать второпях.
В результате даже изначально хорошо структурированный исходник становится беспорядочным и непонятным. Программисты знают, как легко завязнуть в этом хаосе. Причём неважно, чужой это код или собственный.
Чтобы решить все эти проблемы, делается рефакторинг программы. В новом проекте он нужен, чтобы:
Но любое приложение со временем устаревает: язык программирования совершенствуется, появляются новые функции, библиотеки, операторы, делающие код проще и понятнее. То, что год назад требовало пятидесяти строк, сегодня может решаться всего одной.
Поэтому даже идеальная когда-то программа со временем требует нового рефакторинга, обновляющего устаревшие участки кода.
Программный код предназначен не только для компьютера, но и для человека, который будет его дорабатывать. Плохо, если ему придётся неделю разбираться в исходниках, чтобы изменить в программе несколько строк. И не исключено, что этим человеком окажетесь вы сами.
Чем рефакторинг отличается от оптимизации
Рефакторинг — не оптимизация, хотя и может быть с нею связан. Часто его проводят одновременно с оптимизацией, поэтому понятия кажутся синонимами. Но у этих процессов разные цели.
Цель оптимизации — улучшение производительности программы, а рефакторинга — улучшение понятности кода. После оптимизации исходный код может стать сложнее для понимания.
После рефакторинга программа может начать работать быстрее, но главное — её код становится проще и понятнее.
Когда нужно срочно улучшать код
Признаки, показывающие, что назрела необходимость в рефакторинге:
Такой код нужно срочно рефакторить, иначе он будет тормозить реализацию проекта и затруднять внесение правок.
Вообще рефакторинг нужно проводить постоянно. Делайте его каждый раз, после того как поменяли программу и убедились, что всё работает. Например, если добавили или изменили какую-то функцию, метод, класс или объявили новую переменную.
Как делают рефакторинг
Рефакторинг — это маленькие последовательные улучшения кода. Чистить можно всё, но в первую очередь найдите эти проблемы:
После каждой правки посмотрите на соседние участки кода: возможно, их тоже стоит поправить и сделать понятнее. И на те участки кода, которые давно не редактировались, — они уже могли стать некорректными.
Код чистят и на этапе тестирования, когда всё уже готово и проверяется работоспособность программы. Тут разработчик выполняет требования тестировщиков и одновременно проводит рефакторинг.
Не страдайте перфекционизмом! Если вы поправили какой-то кусочек кода, не надо перетряхивать всю программу, разыскивая, что ещё можно улучшить. Стремление к совершенству вечно, но лучше обойтись без фанатизма.
В чём опасности рефакторинга
Мы всё-таки меняем рабочий код. Тут можно не только всё упростить, но и сильно напортачить. Небрежный рефакторинг может отбросить выполнение проекта на дни и недели.
Опасно делать рефакторинг не постоянно, а от случая к случаю. Соблазн сильно улучшить код становится невыносимым. Вы всё глубже закапываетесь в программу и копаете себе яму, в которой легко увязнуть.
Рефакторьте постоянно и по чуть-чуть.
Иногда бывают злоупотребления: рефакторинг может стать способом саботажа, отговоркой, с помощью которой откладываются важные релизы и внедрение новых фич.
Но всё равно нельзя пренебрегать усовершенствованием кода, потому что это лучший способ ускорить работу в будущем.
Хотите создавать эффективные приложения с минимальным количеством ошибок, чистым и понятным кодом? На наших курсах учат не только языкам программирования, но и процессу разработки: проектированию, рефакторингу, тестированию и дебагу.
Что такое рефакторинг кода? Основные принципы и правила рефакторинга
Концепция «рефакторинга» (refactoring) возникла в кругах, связанных со Smalltalk, но вскоре нашла себе дорогу и в лагеря приверженцев других языков программирования. Поскольку рефакторинг является составной частью разработки структуры приложений (framework development), этот термин сразу появляется, когда «структурщики» начинают обсуждать свои дела. Он возникает, когда они уточняют свои иерархии классов и восторгаются тем, на сколько строк им удалось сократить код. Структурщики знают, что хорошую структуру удается создать не сразу — она должна развиваться по мере накопления опыта. Им также известно, что чаще приходится читать и модифицировать код, а не писать новый. В основе поддержки читаемости и модифицируемости кода лежит рефакторинг — как в частном случае структур (frameworks), так и для программного обеспечения в целом.
Так в чем проблема? Только в том, что с рефакторингом связан известный риск. Он требует внести изменения в работающий код, что может привести к появлению трудно находимых ошибок в программе. Неправильно осуществляя рефакторинг, можно потерять дни и даже недели. Еще большим риском чреват рефакторинг, осуществляемый без формальностей или эпизодически. Вы начинаете копаться в коде. Вскоре обнаруживаются новые возможности модификации, и вы начинаете копать глубже. Чем больше вы копаете, тем больше вскрывается нового и тем больше изменений вы производите. В конце концов, получится яма, из которой вы не сможете выбраться. Чтобы не рыть самому себе могилу, следует производить рефакторинг на систематической основе. В книге «Design Patterns» сообщается, что проектные модели создают целевые объекты для рефакторинга. Однако указать цель — лишь одна часть задачи; преобразовать код так, чтобы достичь этой цели, — другая проблема.
Существует несколько методов рефакторинга. Каждый метод описывает мотивацию и технику испытанного на практике преобразования кода. Некоторые виды рефакторинга, такие как «Выделение метода» или «Перемещение поля», могут показаться очевидными, но пусть это не вводит вас в заблуждение. Понимание техники таких методов рефакторинга важно для организованного осуществления рефакторинга. С помощью методов рефакторинга можно поэтапно модифицировать код, внося каждый раз небольшие изменения, благодаря чему снижается риск, связанный с развитием проекта. Эти методы рефакторинга и их названия быстро займут место в вашем словаре разработчика.
Что такое рефакторинг?
Рефакторинг представляет собой процесс такого изменения программной системы, при котором не меняется внешнее поведение кода, но улучшается его внутренняя структура. Это способ систематического приведения кода в порядок, при котором шансы появления новых ошибок минимальны. В сущности, при проведении рефакторинга кода вы улучшаете его дизайн уже после того, как он написан.
«Улучшение кода после его написания» — непривычная фигура речи. В нашем сегодняшнем понимании разработки программного обеспечения мы сначала создаем дизайн системы, а потом пишем код. Сначала создается хороший дизайн, а затем происходит кодирование. Со временем код модифицируется, и целостность системы, соответствие ее структуры изначально созданному дизайну постепенно ухудшаются. Код медленно сползает от проектирования к хакерству.
Рефакторинг представляет собой противоположную практику. С ее помощью можно взять плохой проект, даже хаотический, и переделать его в хорошо спроектированный код. Каждый шаг этого процесса прост до чрезвычайности. Перемещается поле из одного класса в другой, изымается часть кода из метода и помещается в отдельный метод, какой-то код перемещается в иерархии в том или другом направлении. Однако суммарный эффект таких небольших изменений может радикально улучшить проект. Это прямо противоположно обычному явлению постепенного распада программы.
При проведении рефакторинга оказывается, что соотношение разных этапов работ изменяется. Проектирование непрерывно осуществляется во время разработки, а не выполняется целиком заранее. При реализации системы становится ясно, как можно улучшить ее проект. Происходящее взаимодействие приводит к созданию программы, качество проекта которой остается высоким по мере продолжения разработки.
Правила рефакторинга
Самый важный урок, который должен преподать данный пример, это ритм рефакторинга: тестирование, малые изменения, тестирование, малые изменения, тестирование, малые изменения. Именно такой ритм делает рефакторинг быстрым и надежным.
Принципы рефакторинга
Рефакторинг (Refactoring): изменение во внутренней структуре программного обеспечения, имеющее целью облегчить понимание его работы и упростить модификацию, не затрагивая наблюдаемого поведения.
Производить рефакторинг (Refactor): изменять структуру программного обеспечения, применяя ряд рефакторингов, не затрагивая его поведения.
Рефакторинг не меняет видимого поведения программного обеспечения. Оно продолжает выполнять прежние функции. Никто — ни конечный пользователь, ни программист — не сможет сказать по внешнему виду, что что-то изменилось.
Зачем нужно проводить рефакторинг?
Когда следует проводить рефакторинг?
Рефакторингом следует заниматься постоянно понемногу. Надо не решать проводить рефакторинг, а проводить его, потому что необходимо сделать что-то еще, а поможет в этом рефакторинг.
Почему рефакторинг приносит результаты
Из-за чего бывает трудно работать с программами? В данный момент мне приходят в голову четыре причины:
Итак, нам нужны программы, которые легко читать, вся логика которых задана в одном и только одном месте, модификация которых не ставит под угрозу существующие функции и которые позволяют выражать условную логику возможно более простым способом.
Рефакторинг представляет собой процесс улучшения работающей программы не путем изменения ее функций, а путем усиления в ней указанных качеств, позволяющих продолжить разработку с высокой скоростью.
Когда рефакторинг не нужен?
В некоторых случаях рефакторинг вообще не нужен. Основной пример — необходимость переписать программу с нуля. Иногда имеющийся код настолько запутан, что подвергнуть его рефакторингу, конечно, можно, но проще начать все с самого начала.
Явный признак необходимости переписать код — его неработоспособность. Это обнаруживается только при его тестировании, когда ошибок оказывается так много, что сделать код устойчивым не удается. Помните, что перед началом рефакторинга код должен выполняться в основном корректно.
Другой случай, когда следует воздерживаться от рефакторинга, это близость даты завершения проекта. Рост производительности, достигаемый благодаря рефакторингу, проявит себя слишком поздно — после истечения срока. Правильна в этом смысле точка зрения Уорда Каннингема (Ward Cunningham). Незавершенный рефакторинг он сравнивает с залезанием в долги. Большинству компаний для нормальной работы нужны кредиты. Однако вместе с долгами появляются и проценты, то есть дополнительная стоимость обслуживания и расширения, обусловленная чрезмерной сложностью кода. Выплату каких-то процентов можно вытерпеть, но если платежи слишком велики, вы разоритесь. Важно управлять своими долгами, выплачивая их часть посредством рефакторинга.
Однако приближение срока окончания работ — единственный случай, когда можно отложить рефакторинг, ссылаясь на недостаток времени. Опыт работы над несколькими проектами показывает, что проведение рефакторинга приводит к росту производительности труда. Нехватка времени обычно сигнализирует о необходимости рефакторинга.
Рефакторинг и проектирование
Рефакторинг играет особую роль в качестве дополнения к проектированию. Если заранее подумать об архитектуре программы, то можно избежать последующей дорогостоящей переработки. Многие считают, что проектирование важнее всего, а программирование представляет собой механический процесс. Аналогией проекта служит технический чертеж, а аналогией кода — изготовление узла. Но программа весьма отличается от физического механизма. Она значительно более податлива и целиком связана с обдумыванием. Как говорит Элистер Кокберн (Alistair Cockburn):
«При наличии готового дизайна я думаю очень быстро, но в моем мышлении полно пробелов».
Рефакторинг и производительность
С рефакторингом обычно связан вопрос о его влиянии на производительность программы. С целью облегчить понимание работы программы часто осуществляется модификация, приводящая к замедлению выполнения программы. Рефакторинг, несомненно, заставляет программу выполняться медленнее, но при этом делает ее более податливой для настройки производительности. Секрет создания быстрых программ, если только они не предназначены для работы в жестком режиме реального времени, состоит в том, чтобы сначала написать программу, которую можно настраивать, а затем настроить ее так, чтобы достичь приемлемой скорости.
Второй подход предполагает постоянное внимание. В этом случае каждый программист в любой момент времени делает все от него зависящее, чтобы поддерживать высокую производительность программы. Это распространенный и интуитивно привлекательный подход, однако он не так хорош на деле. Модификация, повышающая производительность, обычно затрудняет работу с программой. Это замедляет создание программы. На это можно было бы пойти, если бы в результате получалось более быстрое программное обеспечение, но обычно этого не происходит. Повышающие скорость усовершенствования разбросаны по всей программе, и каждое из них касается только узкой функции, выполняемой программой.
С производительностью связано то интересное обстоятельство, что при анализе большинства программ обнаруживается, что большая часть времени расходуется небольшой частью кода. Если в равной мере оптимизировать весь код, то окажется, что 90% оптимизации произведено впустую, потому что оптимизировался код, который выполняется не слишком часто. Время, ушедшее на ускорение программы, и время, потерянное из-за ее непонятности — все это израсходовано напрасно.
Третий подход к повышению производительности программы основан как раз на этой статистике. Он предполагает создание программы с достаточным разложением ее на компоненты без оглядки на достигаемую производительность вплоть до этапа оптимизации производительности, который обычно наступает на довольно поздней стадии разработки и на котором осуществляется особая процедура настройки программы. Начинается все с запуска программы под профайлером, контролирующим программу и сообщающим, где расходуются время и память. Благодаря этому можно обнаружить тот небольшой участок программы, в котором находятся узкие места производительности. На этих узких местах сосредоточиваются усилия, и осуществляется та же самая оптимизация, которая была бы применена при подходе с постоянным вниманием. Но благодаря тому, что внимание сосредоточено на выявленных узких местах, удается достичь больших результатов при значительно меньших затратах труда. Но даже в этой ситуации необходима бдительность. Как и при проведении рефакторинга, изменения следует вносить небольшими порциями, каждый раз компилируя, тестируя и запуская профайлер. Если производительность не увеличилась, изменениям дается обратный ход. Процесс поиска и ликвидации узких мест продолжается до достижения производительности, которая удовлетворяет пользователей.
Разработка тестов
При проведении рефакторинга важным предварительным условием является наличие надежных тестов.
Правила разработки тестов