Перевод статьи «Debugging — you’re doing it wrong. 10 techniques to find a bug in your code».
Помните эти долгие, долгие часы, проведенные за отладкой? Когда вы всматриваетесь в код и не можете понять, что именно не в порядке? Вы не одиноки! Думаю, время от времени трудности с отладкой переживают все разработчики. В этой статье я расскажу вам о своих любимых подходах к поиску багов в коде.
Поискать информацию по сообщению об ошибке в Google
Я не буду сортировать свои подходы по степени их полезности, но поиск в Google занимает первое место совершенно заслуженно. Думаю, что когда, взглянув на сообщение об ошибке, вы даже не можете его понять, загуглить это самый быстрый способ разобраться, что к чему.
В большинстве случаев сообщение, с которым вы столкнулись, наверняка гуглил и кто-нибудь еще. Кроме того, у нас есть много таких прекрасных мест как StackOverflow и GitHub issues, где люди помогают друг другу. Благодаря этому вы можете найти не только ответ, но и советы о том, что нужно предпринять, чтобы подобная ошибка не возникала в дальнейшем. Так почему же не гуглить ошибки? Это самый простой способ из всех!
Как тестировать веб сайт? | Как найти баги на сайте?
Console log
Я полагаю, это один из самых популярных способов поиска багов в кодовой базе. Мы добавляем предложения console.log(…) в код, заново запускаем приложение и пытаемся разобраться, что идет не так.
Я люблю этот способ и, как мне кажется, это подходящий вариант для поиска простых проблем, уже локализованных в нескольких классах. Но если вы вообще не представляете, что происходит и где притаился баг, начинать поиски при помощи console.log(…) будет плохой идеей. Потому что в этом случае можно пропустить что-то важное, что не попало в логи, и тогда вам придется многократно добавлять console log и перезапускать приложение, пока вы не найдете причину отказа.
Использовать отладчик
Не думаю, что здесь нужно что-то объяснять. Все разработчики знают, что такое отладчик и как им пользоваться. Вместо этого я расскажу вам о своем недавнем маленьком разговоре на эту тему с коллегой.
Несколько дней назад мы с товарищами по работе обсуждали разные подходы к отладке (что и привело к написанию этой статьи). И я сказал, что самый надежный способ найти корень проблемы – использовать отладчик. Нужно лишь установить точки прерывания, а затем, производя какие-то действия, продвигаться шаг за шагом по коду, наблюдая за изменениями в приложении. Что может быть проще?
Но один из моих коллег высказал потрясающую мысль: используя отладчик, вы не тренируете свои аналитические способности и критическое мышление. С отладчиком вы лишь смотрите в окно с переменными и ждете неправильного поведения. А если вы копаетесь в кодовой базе без отладчика, вы более сфокусированы. Вы пытаетесь понять, что происходит, и таким образом каждый раз открываете для себя что-то новое.
ПОИСК БАГОВ НА РЕАЛЬНОМ ПРИМЕРЕ
Локализация проблемы
Основная идея метода локализации проблемы в том, чтобы пошагово удалять или закрывать комментариями код, пока не поймете, в каком месте у вас ошибка. Это особенно полезно в тех случаях, когда вы довольно длительное время пишете алгоритм или какую-то бизнес-логику, не компилируя и не выполняя приложение.
В подобных случаях я всегда делаю что-то не так, и самый простой способ найти, где именно вкралась ошибка, это частично закомментировать код и проследить, исчезла ли ошибка. Затем процесс следует повторять до тех пор, пока все баги не будут найдены и исправлены.
В некоторых случаях бывает полезным использовать подход бинарного поиска: закомментировать половину кода (или удалить половину файлов) и посмотреть, осталась ли ошибка. Если да, то повторить процесс для половины этой половины, если нет – для другой половины кода.
Конечно, это работает лишь в определенных случаях, когда вы можете закомментировать или удалить часть кода таким образом, чтобы все остальное работало.
Создать несколько тестов
Да, это бредовый вариант, но и он может быть полезен в определенных случаях. С моей точки зрения, этот способ хорошо работает, когда у вас есть какой-то неверно работающий алгоритм, слишком сложный, чтобы писать console.log или проходить построчно с отладчиком.
В этом случае может быть полезным написание пары тестов для этого кода. Они могут помочь вам локализовать проблему в алгоритме.
Когда баг будет локализован, вы будете знать, где искать, и для дальнейшего поиска сможете использовать другой подход.
Анализ логов
Да, я знаю, что вы терпеть не можете анализировать все эти 10mb текстовых файлов с логами. Но довольно часто это позволяет сэкономить целые часы на отладке. Конечно, прежде всего следует адекватно настроить ведение логов. Файлы с логами должны собираться и храниться какое-то разумное количество времени. Но если соблюдены все условия – вам повезло.
Это как использование console log, только у вас уже есть все console.log-и на своих местах, так что вы можете просто читать, какие действия выполняла система.
Но, к сожалению, довольно часто об этом остается лишь мечтать. Мы не всегда уделяем достаточно внимания ведению логов.
Спросить у друга
Это вполне очевидный вариант, но мы часто им пренебрегаем. Полагаю, у всех нас в офисе есть более опытные коллеги. А если это не так, можно найти нужных экспертов в интернете. Не стесняйтесь обращаться за помощью: люди всегда готовы вам помочь!
Но. Прежде, чем спрашивать у кого-то, сначала необходимо просмотреть все доступные ресурсы самостоятельно. Если будете спрашивать у людей какую-то легкотню, написанную на второй странице документации, – будьте готовы спасаться бегством!
Git bisect
Git не только помогает нам отслеживать историю изменений в приложении, но и предоставляет несколько инструментов для отладки. Одни из них – git bisect – инструмент для осуществления бинарного поиска по вашей git-истории. Это довольно полезно в случаях, когда вы некоторое время не работали с этой кодовой базой, а за это время в ней были добавлены сотни коммитов. И теперь вы обнаружили баг и понятия не имеете, когда именно он появился. Но вы помните, что, например, в версии 2.0.15 его не было.
В этом случае git bisect вам поможет. Идея тут довольно простая. Вы начинаете процесс отладки (git bisect start), затем нужно пометить текущую версию как плохую, потому что здесь у нас баг (git bisect bad). После этого нужно сообщить git, какую версию считать хорошей – git bisect good 2.0.15. На этой стадии настройка завершена и мы можем начинать поиск.
git bisect выбирает коммит на середине отрезка bad-good и осуществляет проверку. Нам нужно проверить, есть ли баг в этой версии. Если да – запускаем git bisect bad, если нет – git bisect good. Затем git выбирает новый коммит на оригинальном отрезке bad-good и мы повторяем процесс, пока не найдем коммит с багом.
git bisect это очень мощный инструмент, поэтому полностью описывать его здесь мы не будем. Если вам интересно, хорошее пояснение есть здесь.
Поговорить с уточкой
Это один из самых действенных методов понять, что происходит в коде. Суть его в следующем. Вы берете резиновую уточку (любую игрушку), ставите ее перед собой и объясняете ей всю вашу систему, начиная с общих концепций и продвигаясь строчка за строчкой. Мне нравится этот метод и я пользуюсь им регулярно.
Когда-то, на заре моей деятельности в качестве разработчика, я долго не мог разобраться с одной проблемой. Несколько часов я пялился в экран, а потом решил обратиться за помощью к коллеге. Я начал объяснять ему ситуацию и уже через минуту нашел решение! Тогда я еще не знал о методе утенка, но вполне ощутил, насколько этот метод действенный.
Танцы с бубном
Это мой любимый способ решения проблем с программами. Все, что вам нужно, это бубен. Танцуйте вокруг своего рабочего стола, ударяя в бубен. Большой плюс, если на бубне нанесен логотип технологии, с которой вы работаете. Например, у меня есть бубен для решения проблем с микросервисами:
Правда, для фронтенд-приложений он совершенно бесполезен.
Наконец, для тех, кто дочитал до конца, отмечу, что самый лучший способ решения проблем с багами – писать код без багов. Вам это любой менеджер подтвердит:)
Но, если серьезно, будьте сфокусированы на проблеме и последовательны. Исследуйте свой код шаг за шагом, применяя различные подходы, и вы эффективно расправитесь со всеми вашими багами.
Источник: techrocks.ru
Где искать баги фаззингом и откуда вообще появился этот метод
Подход фаззинг-тестирования родился еще в 80-х годах прошлого века. В некоторых языках он используется давно и плодотворно — соответственно, уже успел занять свою нишу. Сторонние фаззеры для Go были доступны и ранее, но в Go 1.18 появился стандартный. Мы в «Лаборатории Касперского» уже успели его пощупать и тестируем с его помощью довольно большой самостоятельный сервис.
Меня зовут Владимир Романько, я — Development Team Lead, и именно моя команда фаззит баги на Go. В этой статье я расскажу про историю фаззинга, про то, где и как искать баги, а также как помочь фаззинг-тестам эффективнее находить их в самых неожиданных местах. И покажу этот подход на примере обнаружения SQL-инъекций.
Немного истории
Когда я первый раз услышал о фаззинге, сама идея прозвучала для меня довольно странно. Казалось, это магия, с помощью которой сторонняя программа может найти баги в моем коде.
Все встало на свои места, когда я узнал, как фаззинг появился. Поэтому свой рассказ я также хочу начать с интересной истории, которая, вероятно, ждет своего Кристофера Нолана для экранизации. В ней есть все необходимые компоненты отличного голливудского блокбастера: зловещая ночь с грозой, гениальный ученый, а также древний артефакт, который принял во всем этом участие. Забегая вперед, отмечу, что роль древнего артефакта исполнил модем на 1200 бод. В итоге случайное стечение обстоятельств привело к появлению хорошего изобретения.
Так что же произошло?
Тем ученым был Бартон Миллер. В 1988 году он работал профессором в университете и решил из дома подключиться через модем к своему любимому университетскому мейнфрейму. Он пытался выполнить команду Unix… История умалчивает о том, какая именно это была команда. Предположим, это был grep.
grep -R «hello world»
В этот момент где-то недалеко ударила молния. А старый модем если и имел код коррекции ошибок, тот оказался недостаточно эффективным. Вместо «Hello world» до мейнфрейма долетел мусор:
grep -R «hello ~3#зеШwкACh»
Который внезапно вызвал segmentation fault. И grep упал.
Бартон Миллер задумался, почему это произошло. Grep к тому времени уже был старой, надежной, многократно протестированной командой, у которой явно есть какие-то проверки ввода. Но тем не менее он упал. И ученому пришла в голову идея написать программу, которая специально будет генерировать мусор и отправлять на вход в различные unix-овые утилиты. Так появился первый фаззер.
Вместе со студентами Бартон Миллер нашел очень много ошибок в командах Unix. К этому моменту все эти команды уже использовались инженерами по всему миру в течение длительного времени, но тем не менее содержали ошибки. Такова суперспособность фаззера, за которую мы его и любим, — находить баги в хорошо протестированном коде.
Как фаззер находит баги
Тесты можно классифицировать по-разному. Но давайте распределим их по уровню семантического знания о коде, которое использовано при написании теста.
Больше всего знаний о тестируемом коде используется при построении тестов по тест-кейсам, например в юнит-, ручных или интеграционных тестах. Они содержат некие пред- и постусловия — конкретные проверки того, что на вход программы мы передали А, а на выходе должны получить Б. Для написания таких тестов однозначно придется изучить код и требования к нему.
Левее по этой шкале находятся property based тесты. В них уже нет конкретных входов и выходов. Данные на вход генерируются случайным образом, но проверяются определенные свойства кода (поэтому тесты и называются property based). К примеру, если мы проверяем функцию сортировки, на выходе ожидаем массив, каждый последующий элемент которого больше либо равен предыдущему.
В крайней левой части шкалы находятся фаззинг-тесты, имеющие минимальные знания о продукте. Им известно только то, что код не должен падать, зависать или отъедать какое-то безумное количество памяти. С точки зрения теста сам продукт представляет собой черный ящик.
В том, что при фаззинге используются минимальные знания, есть как плюсы, так и минусы. Если тесты по тест-кейсам позволяют находить так называемые known unknowns (т. е. проверяют неизвестное поведение в известных сценариях), то фаззинг ищет unknown unknowns (проверяет неизвестное поведение в неизвестных сценариях). Именно эта особенность позволяет фаззингу находить баги в хорошо протестированном коде — он обнаруживает сценарии, про которые разработчик никогда и не подумал бы.
Для примера приведу тесты для функции сортировки, которая принимает слайс int.
Функцию можно протестировать с помощью тестов по тест-кейсам. В этом случае на вход мы передаем (2, 1, 3) и проверяем ожидание, что на выходе будет 1, 2, 3.
В property based тестах на входе случайная последовательность, а на выходе надо убедиться, что каждый последующий элемент больше или равен предыдущему (т. е. проверяется свойство).
Фаззинг также передаст случайную последовательность, но удостоверится, что функция не упала.
Фаззинг не заменяет классическое тестирование. Он нужен, когда проверяется уже оттестированный код, а фантазия тестировщиков подходит к концу. В общем случае фаззер найдет меньше ошибок, чем тесты по тест-кейсам. Но эти ошибки будут наиболее разнообразны.
Это хорошо заметно в Go-шной реализации фаззера — go fuzz, которая модифицирует корпус входных тестовых данных так, чтобы отработали все ветви кода. Стандартный фаззер Go вообще одинаково хорошо подходит как для написания property based тестов, так и для классического фаззинга. Официальная документация Go по фаззингу не делает различия между этими видами тестов.
Как помочь фаззеру
Давайте рассмотрим нехитрую программу на Go.
Наш код объявляет неинициализированный нулевой указатель и что-то по нему пишет.
Если запустить этот код, он упадет с ошибкой. Проблема в пятой строке.
Чисто теоретически ее можно было бы проигнорировать и продолжить выполнение. Если кто-то помнит, в Visual Basic был такой режим: при ошибке программа не падала, а просто переходила к следующей строке кода. Получалось, что они надежны, но поведение этих программ непредсказуемо. Это никак не помогло бы фаззеру.
Вернемся к Бартону Миллеру. Что было бы, если бы grep проигнорировал обращение к невалидному указателю? Скорее всего, фаззер не нашел бы багов в команде. Команда обработала бы мусор на входе и выдала мусор на выходе. Никто не понял бы, что произошло нечто плохое.
Т. е. фаззер в принципе смог найти ошибку только благодаря крэшу (программа проверила свой собственный внутренний инвариант, согласно которому нельзя обращаться к некорректному указателю, и упала, когда он оказался нарушен).
Так мы приходим к выводу: падать с ошибкой полезно.
Чем больше код проверяет своих внутренних инвариантов, тем больше фаззер может найти багов.
- Инвариант может заключаться в том, что оба потомка красного узла в красно-черном дереве — черные. Если он нарушен, код может как-то сообщить об этом фаззеру.
- Можно проверять, что количество элементов в контейнере неотрицательно — в очереди не может содержаться «-1» элемент.
- Проверка может выявлять, что в стеке количество операций Pop меньше или равно количеству Push.
- Бизнес-логика может контролировать отсутствие превышения некоего программного лимита. Тот факт, что мы обнаружим нарушение этого инварианта, будет свидетельствовать об ошибке в бизнес-логике.
- Кэш должен отсекать повторные запросы. Это особенно актуально, если база вычисляет тяжелые запросы и нужно проверить бизнес-логику, которая хранит в памяти результаты нескольких последних запросов. В этом случае фаззер может найти ошибку в логике работы с кэшем.
- SQL-запрос не должен возвращать ошибку некорректного синтаксиса. Если же мы получаем такую ошибку, в нашем коде однозначно есть проблема. Скорее всего, мы неправильно формируем тело SQL-запроса или в это тело без какой-либо санитизации попадает пользовательский ввод (SQL-инъекция).
Инвариант мало проверить, нужно еще донести до фаззера информацию о том, что есть нарушение. Самый простой способ — кинуть panic. Это можно сделать с любого уровня абстракции.
- Кидать panic не всегда, а только в специальном фаззинг-режиме: ввести переменную окружения, и если она задана, при нарушении инвариантов кидать panic. Фаззинг-режим, кстати, помогает решить проблему с «дорогими» проверками инвариантов, когда на них уходит много ресурсов.
- Можно использовать специальный уровень логирования, доступный фаззеру. Когда тот увидит запись с этим уровнем, он поймет, что нарушен некий внутренний инвариант (и сделает вывод, что код написан некорректно). Этот подход требует более сложной инфраструктуры. И важно не писать в лог на этом уровне, когда наблюдаются проблемы с инфраструктурой (например, если отвалилась сеть).
Фаззинг имеет смысл, если функции работают достаточно быстро. Если функция выдает ответ через несколько минут после передачи в нее начальных данных, за разумное время фаззер просто не успеет ничего проверить. А проверив слишком мало вариантов, он ничего не найдет. Возможно, в этой ситуации процесс ускорят моки, но работать с ними надо всегда очень осторожно. Также можно гонять тесты на маленьких частях кода.
Помогаем фаззеру искать SQL-уязвимости
Теперь применим этот подход, чтобы обрабатывать ошибки работы с базой данных на примере SQLite.
Предположим, у нас есть нативный код, в котором присутствует уязвимость SQL-инъекции. Мы формируем строку, и если получаем ошибку, смотрим, что это было. Если это ошибка SQLite, которая говорит, что синтаксис некорректный, и при этом включена переменная окружения FUZZING, мы кидаем panic. В ином случае мы обрабатываем ошибки стандартным путем.
Каждый раз писать такой код неудобно, поэтому его можно разбить на вспомогательные функции и уже их использовать по всему коду.
Я бы предложил такой вариант разбиения.
Home
Подход фаззинг-тестирования родился еще в 80-х годах прошлого века. В некоторых языках он используется давно и плодотворно — соответственно, уже успел занять свою нишу. Сторонние фаззеры для Go были доступны и ранее, но в Go 1.18 появился стандартный. Мы в «Лаборатории Касперского» уже успели его пощупать и тестируем с его помощью довольно большой самостоятельный сервис.
Меня зовут Владимир Романько, я — Development Team Lead, и именно моя команда фаззит баги на Go. В этой статье я расскажу про историю фаззинга, про то, где и как искать баги, а также как помочь фаззинг-тестам эффективнее находить их в самых неожиданных местах. И покажу этот подход на примере обнаружения SQL-инъекций.
Немного истории
Когда я первый раз услышал о фаззинге, сама идея прозвучала для меня довольно странно. Казалось, это магия, с помощью которой сторонняя программа может найти баги в моем коде.
Все встало на свои места, когда я узнал, как фаззинг появился. Поэтому свой рассказ я также хочу начать с интересной истории, которая, вероятно, ждет своего Кристофера Нолана для экранизации. В ней есть все необходимые компоненты отличного голливудского блокбастера: зловещая ночь с грозой, гениальный ученый, а также древний артефакт, который принял во всем этом участие. Забегая вперед, отмечу, что роль древнего артефакта исполнил модем на 1200 бод. В итоге случайное стечение обстоятельств привело к появлению хорошего изобретения.
Так что же произошло?
Тем ученым был Бартон Миллер. В 1988 году он работал профессором в университете и решил из дома подключиться через модем к своему любимому университетскому мейнфрейму. Он пытался выполнить команду Unix… История умалчивает о том, какая именно это была команда. Предположим, это был grep.
grep -R «hello world»
В этот момент где-то недалеко ударила молния. А старый модем если и имел код коррекции ошибок, тот оказался недостаточно эффективным. Вместо «Hello world» до мейнфрейма долетел мусор:
grep -R «hello ~3#зеШwкACh»
Который внезапно вызвал segmentation fault. И grep упал.
Бартон Миллер задумался, почему это произошло. Grep к тому времени уже был старой, надежной, многократно протестированной командой, у которой явно есть какие-то проверки ввода. Но тем не менее он упал. И ученому пришла в голову идея написать программу, которая специально будет генерировать мусор и отправлять на вход в различные unix-овые утилиты. Так появился первый фаззер.
Вместе со студентами Бартон Миллер нашел очень много ошибок в командах Unix. К этому моменту все эти команды уже использовались инженерами по всему миру в течение длительного времени, но тем не менее содержали ошибки. Такова суперспособность фаззера, за которую мы его и любим, — находить баги в хорошо протестированном коде.
Как фаззер находит баги
Тесты можно классифицировать по-разному. Но давайте распределим их по уровню семантического знания о коде, которое использовано при написании теста.
Больше всего знаний о тестируемом коде используется при построении тестов по тест-кейсам, например в юнит-, ручных или интеграционных тестах. Они содержат некие пред- и постусловия — конкретные проверки того, что на вход программы мы передали А, а на выходе должны получить Б. Для написания таких тестов однозначно придется изучить код и требования к нему.
Левее по этой шкале находятся property based тесты. В них уже нет конкретных входов и выходов. Данные на вход генерируются случайным образом, но проверяются определенные свойства кода (поэтому тесты и называются property based). К примеру, если мы проверяем функцию сортировки, на выходе ожидаем массив, каждый последующий элемент которого больше либо равен предыдущему.
В крайней левой части шкалы находятся фаззинг-тесты, имеющие минимальные знания о продукте. Им известно только то, что код не должен падать, зависать или отъедать какое-то безумное количество памяти. С точки зрения теста сам продукт представляет собой черный ящик.
В том, что при фаззинге используются минимальные знания, есть как плюсы, так и минусы. Если тесты по тест-кейсам позволяют находить так называемые known unknowns (т. е. проверяют неизвестное поведение в известных сценариях), то фаззинг ищет unknown unknowns (проверяет неизвестное поведение в неизвестных сценариях). Именно эта особенность позволяет фаззингу находить баги в хорошо протестированном коде — он обнаруживает сценарии, про которые разработчик никогда и не подумал бы.
Для примера приведу тесты для функции сортировки, которая принимает слайс int.
Функцию можно протестировать с помощью тестов по тест-кейсам. В этом случае на вход мы передаем (2, 1, 3) и проверяем ожидание, что на выходе будет 1, 2, 3.
В property based тестах на входе случайная последовательность, а на выходе надо убедиться, что каждый последующий элемент больше или равен предыдущему (т. е. проверяется свойство).
Фаззинг также передаст случайную последовательность, но удостоверится, что функция не упала.
Фаззинг не заменяет классическое тестирование. Он нужен, когда проверяется уже оттестированный код, а фантазия тестировщиков подходит к концу. В общем случае фаззер найдет меньше ошибок, чем тесты по тест-кейсам. Но эти ошибки будут наиболее разнообразны.
Это хорошо заметно в Go-шной реализации фаззера — go fuzz, которая модифицирует корпус входных тестовых данных так, чтобы отработали все ветви кода. Стандартный фаззер Go вообще одинаково хорошо подходит как для написания property based тестов, так и для классического фаззинга. Официальная документация Go по фаззингу не делает различия между этими видами тестов.
Как помочь фаззеру
Давайте рассмотрим нехитрую программу на Go.
Наш код объявляет неинициализированный нулевой указатель и что-то по нему пишет.
Если запустить этот код, он упадет с ошибкой. Проблема в пятой строке.
Чисто теоретически ее можно было бы проигнорировать и продолжить выполнение. Если кто-то помнит, в Visual Basic был такой режим: при ошибке программа не падала, а просто переходила к следующей строке кода. Получалось, что они надежны, но поведение этих программ непредсказуемо. Это никак не помогло бы фаззеру.
Вернемся к Бартону Миллеру. Что было бы, если бы grep проигнорировал обращение к невалидному указателю? Скорее всего, фаззер не нашел бы багов в команде. Команда обработала бы мусор на входе и выдала мусор на выходе. Никто не понял бы, что произошло нечто плохое.
Т. е. фаззер в принципе смог найти ошибку только благодаря крэшу (программа проверила свой собственный внутренний инвариант, согласно которому нельзя обращаться к некорректному указателю, и упала, когда он оказался нарушен).
Так мы приходим к выводу: падать с ошибкой полезно.
Чем больше код проверяет своих внутренних инвариантов, тем больше фаззер может найти багов.
Вот несколько примеров с проверкой внутренних инвариантов:
- Инвариант может заключаться в том, что оба потомка красного узла в красно-черном дереве — черные. Если он нарушен, код может как-то сообщить об этом фаззеру.
- Можно проверять, что количество элементов в контейнере неотрицательно — в очереди не может содержаться «-1» элемент.
- Проверка может выявлять, что в стеке количество операций Pop меньше или равно количеству Push.
- Бизнес-логика может контролировать отсутствие превышения некоего программного лимита. Тот факт, что мы обнаружим нарушение этого инварианта, будет свидетельствовать об ошибке в бизнес-логике.
- Кэш должен отсекать повторные запросы. Это особенно актуально, если база вычисляет тяжелые запросы и нужно проверить бизнес-логику, которая хранит в памяти результаты нескольких последних запросов. В этом случае фаззер может найти ошибку в логике работы с кэшем.
- SQL-запрос не должен возвращать ошибку некорректного синтаксиса. Если же мы получаем такую ошибку, в нашем коде однозначно есть проблема. Скорее всего, мы неправильно формируем тело SQL-запроса или в это тело без какой-либо санитизации попадает пользовательский ввод (SQL-инъекция).
Все эти примеры объединяет тот факт, что если проверка сработает, это однозначно указывает на некорректно написанный код. Это никак не связано с окружением. По сути это ничем не отличается от обращения к невалидному указателю, упомянутому выше.
Инвариант мало проверить, нужно еще донести до фаззера информацию о том, что есть нарушение. Самый простой способ — кинуть panic. Это можно сделать с любого уровня абстракции.
Среди Go-феров есть предубеждение против panic, поэтому можно использовать более лайтовые варианты:
- Кидать panic не всегда, а только в специальном фаззинг-режиме: ввести переменную окружения, и если она задана, при нарушении инвариантов кидать panic. Фаззинг-режим, кстати, помогает решить проблему с «дорогими» проверками инвариантов, когда на них уходит много ресурсов.
- Можно использовать специальный уровень логирования, доступный фаззеру. Когда тот увидит запись с этим уровнем, он поймет, что нарушен некий внутренний инвариант (и сделает вывод, что код написан некорректно). Этот подход требует более сложной инфраструктуры. И важно не писать в лог на этом уровне, когда наблюдаются проблемы с инфраструктурой (например, если отвалилась сеть).
При проверке инвариантов у фаззинг-теста нет никакого ожидания относительно кода. За счет этого он более стабильный — его не нужно менять при малейших изменениях. С развитием продукта его также нужно поддерживать, но усилий на это будет уходить гораздо меньше, чем в случае с тестами по тест-кейсам. Возвращаясь к примеру с grep — если мы сменим движок обработки регулярных выражений, суть теста никак не изменится.
Фаззинг имеет смысл, если функции работают достаточно быстро. Если функция выдает ответ через несколько минут после передачи в нее начальных данных, за разумное время фаззер просто не успеет ничего проверить. А проверив слишком мало вариантов, он ничего не найдет. Возможно, в этой ситуации процесс ускорят моки, но работать с ними надо всегда очень осторожно. Также можно гонять тесты на маленьких частях кода.
Помогаем фаззеру искать SQL-уязвимости
Теперь применим этот подход, чтобы обрабатывать ошибки работы с базой данных на примере SQLite.
Предположим, у нас есть нативный код, в котором присутствует уязвимость SQL-инъекции. Мы формируем строку, и если получаем ошибку, смотрим, что это было. Если это ошибка SQLite, которая говорит, что синтаксис некорректный, и при этом включена переменная окружения FUZZING, мы кидаем panic. В ином случае мы обрабатываем ошибки стандартным путем.
Каждый раз писать такой код неудобно, поэтому его можно разбить на вспомогательные функции и уже их использовать по всему коду.
Я бы предложил такой вариант разбиения.