Найдите и исправьте небезопасные запросы к базе данных

SQLiteBeginner
Практиковаться сейчас

Введение

Имя, переданное пользователем, должно оставаться значением, а не становиться частью команды базы данных. Сейчас поиск DeskLite строит текст SQL непосредственно из имени. Обычные запросы выглядят рабочими, но имена с апострофами и специально сформированный ввод обнаруживают проблему.

Вы прочитаете всего пятистрочную функцию Python, изучите небольшой используемый ею фрагмент SQL, разберёте реальную находку Semgrep и примените один шаблон привязки параметров SQLite. Предварительные знания Python или SQL не нужны. Средство запуска, база данных, сканер и функциональная проверка уже подготовлены.

Выполните обычный поиск

На этом шаге вы определите, что должен возвращать поиск, прежде чем читать его реализацию.

DeskLite хранит три вымышленные заявки в небольшой базе SQLite. SQLite — движок базы данных, который читает локальный файл и выполняет запросы к нему. Соединение и данные уже подготовлены в курсе.

cd /home/labex/project/ticket-queries

run_lookup.py — подготовленная точка входа. Как в лабораторной работе по зависимостям, python3 запускает предоставленную программу. Последний аргумент — имя для поиска:

python3 run_lookup.py Alice

Ожидается одна заявка с идентификатором 1 и заголовком Printer offline. Средство запуска оформляет строки базы данных в знакомый JSON-ответ. Вы будете редактировать небольшую функцию поиска, а не код форматирования средства запуска.

Задуманная граница проста: переданное имя выбирает заявки этого владельца. Оно не должно менять смысл команды базы данных или раскрывать заявки других владельцев. Все имена и записи в упражнении вымышлены; работайте только с этой подготовленной локальной базой.

Прочитайте пятистрочную функцию

На этом шаге вы проследите путь переданного имени через небольшую функцию Python.

cat lookup.py

Файл содержит:

from fixture import db

def lookup(name):
    query = f"SELECT id, title FROM tickets WHERE owner = '{name}'"
    return db.execute(query).fetchall()

from fixture import db делает подготовленное соединение с базой данных доступным под именем db. Функция — именованный фрагмент поведения, который можно использовать повторно. def lookup(name): определяет функцию со входным значением под именем name. Строки с отступами принадлежат этой функции; сохраняйте их отступы при редактировании.

Переменная даёт имя значению. query = ... присваивает текстовое значение, называемое строкой, переменной query. Начальная f делает её f-строкой: Python вставляет текущее значение name в позицию {name}. Если ввод — Alice, этот текст становится частью SQL-строки.

db.execute(query) — похожий на вызов функции вызов метода, который просит соединение выполнить запрос. .fetchall() собирает строки результата. return передаёт эти строки вызывающему коду. Подготовленное средство запуска передаёт ваш аргумент терминала в lookup и показывает возвращённые строки.

Вам нужен только этот путь данных: переданное имя → строка запроса → вызов базы данных → возвращённые строки. Далее вы прочитаете, что означает эта SQL-строка.

Прочитайте задуманное условие SQL

На этом шаге вы свяжете условие SQL с записями, которые должны возвращаться.

SQL — язык задания операций базы данных. Этот запрос использует лишь небольшую его часть:

SELECT id, title FROM tickets WHERE owner = 'Alice'

SELECT id, title выбирает два поля результата. FROM tickets выбирает подготовленную таблицу. WHERE owner = 'Alice' ограничивает результат строками, чей владелец равен этому текстовому значению. В SQL одинарные кавычки обозначают текстовый литерал. = внутри этой SQL-строки выражает условие для владельца. Присваивание Python query = ... вне строки даёт готовому тексту имя переменной.

Попробуйте другое обычное имя:

python3 run_lookup.py Bob

Ожидается только заявка с идентификатором 3 и заголовком Spare keyboard. Имя меняет задуманное значение условия владельца, а не выбранные столбцы или таблицу.

Теперь свяжите это с прочитанной f-строкой. Python вставляет переданное имя между кавычками SQL до того, как SQLite увидит запрос. Это объединяет две разные вещи: структуру команды и входные данные. Кавычка в настоящем имени может нарушить эти границы.

Пока не редактируйте. Сначала вы используете фиксированный сканер, чтобы найти рискованную конструкцию, затем подтвердите поведение с допустимым именем с апострофом и разрешённым локальным тестовым вводом.

Разберите находку статического анализа

На этом шаге вы прочитаете реальную находку Semgrep и свяжете указанное место с построением запроса.

Статическое тестирование безопасности приложений (SAST) исследует код без запуска со всеми возможными входными данными. Подготовленная команда запуска course-code-scan использует закреплённый движок Semgrep CE 1.145.0 и фиксированное локальное правило. Облачная учётная запись или сетевой набор правил не используются.

course-code-scan lookup.py

Ожидается находка в lookup.py:5 с идентификатором правила desklite.sql-parameter-binding и таким объяснением:

Bind the supplied lookup name as a SQL parameter instead of building SQL text from it.

Строка указывает на использование query в вызове базы данных. Проследите это значение на одну строку выше, чтобы увидеть построение f-строки. Это короткое исследование потока данных, а не предложение разработать целое приложение.

Находка сопровождается кодом завершения 1; CHECK ERROR сопровождается 2, когда файл невозможно проанализировать. Ни то ни другое не является чистым результатом. Локальное правило отслеживает попадание предоставленного в курсе name в текст SQL. Его область ограничена: находка всё ещё требует проверки кода, а отсутствие находок по одному правилу не доказывает отсутствие других рисков приложения.

Далее вы сравните находку с реальным поведением поиска.

Сравните текст с апострофом и ввод, меняющий запрос

На этом шаге вы увидите, почему ввод должен оставаться данными, используя только подготовленные локальные тестовые данные.

Одного настоящего владельца зовут O'Neil. Двойные кавычки удерживают весь аргумент терминала вместе, включая апостроф:

python3 run_lookup.py "O'Neil"

Ожидается строка, начинающаяся с Lookup failed:, с синтаксической ошибкой SQLite. Эта ошибка — намеренное подтверждение дефекта. Апостроф в имени завершил текстовый литерал SQL, потому что программа вставила его прямо в текст команды. Запрет всех апострофов сломал бы поиск настоящего владельца.

Теперь попробуйте этот ограниченный тестовый ввод:

python3 run_lookup.py "' OR 1=1 --"

Небезопасная версия возвращает все три заявки вместо отсутствия совпадающего владельца. В этом контексте SQL кавычка закрывает литерал, OR 1=1 делает условие истинным, а -- превращает оставшийся текст в комментарий. Ввод изменил структуру команды вместо того, чтобы остаться именем. Это SQL-инъекция.

Вы не тестируете внешний сервис и не меняете данные. Подготовленная база доступна только для чтения и содержит вымышленные записи. Требуемое исправление должно удовлетворять обоим наблюдениям: допустимое имя с апострофом работает, а текст, меняющий запрос, не может расширить набор возвращаемых строк.

Привяжите имя отдельно от текста SQL

На этом шаге вы исправите поиск с помощью интерфейса привязки параметров SQLite.

Заполнитель отмечает место для значения данных. Заполнитель SQLite ? — часть фиксированной структуры SQL; имя передаётся в execute отдельно. Драйвер базы данных обрабатывает переданное значение как данные, даже если оно содержит кавычки или символы, похожие на SQL.

nano lookup.py

Замените строки запроса и выполнения функции этим поддерживаемым шаблоном, сохранив импорт, имя функции и отступы. Сохраните с помощью Ctrl+O, Enter, Ctrl+X:

from fixture import db

def lookup(name):
    query = "SELECT id, title FROM tickets WHERE owner = ?"
    return db.execute(query, (name,)).fetchall()

Не заключайте ? в кавычки SQL. Представлением значения занимается драйвер. (name,) — одноэлементный кортеж, упорядоченная коллекция значений. Завершающая запятая делает его кортежем; его единственное значение соответствует единственному заполнителю. Второй аргумент execute передаёт этот кортеж отдельно от текста запроса.

Выполните обычный поиск:

python3 run_lookup.py Alice

Затем повторите поиск допустимого имени с апострофом:

python3 run_lookup.py "O'Neil"

Ожидаются заявка с идентификатором 1 для Alice и заявка с идентификатором 2 и заголовком Login issue для O'Neil. Обе используют одну фиксированную структуру SQL. Если Python сообщает об ошибке отступов или синтаксиса, восстановите короткую функцию выше; не меняйте базу данных или средство запуска.

Проверьте и отклонение, и допустимое поведение

На этом шаге вы убедитесь, что исправление сохраняет допустимые имена и не позволяет переданному тексту расширить поиск.

Повторите ввод, меняющий запрос:

python3 run_lookup.py "' OR 1=1 --"

Теперь ожидается пустой список tickets. SQLite ищет владельца с именем, буквально равным этому тексту, а такого владельца нет. Структура запроса не была дополнена.

Проверьте обычного отсутствующего владельца:

python3 run_lookup.py "Missing owner"

Ожидается ещё один пустой список. Отсутствующее значение отличается от сломанного приложения: обычные имена и допустимые имена с апострофами всё ещё возвращают свои заявки.

Запустите предоставленную функциональную регрессионную проверку тем же способом, что и в лабораторной работе по зависимостям:

bash test.sh

Ожидается:

PASS: ordinary, quoted, missing, and injection-like names.

Эти проверки выполняют реальные вызовы SQLite и проверяют строки результата. Удаление функции, возврат пустого списка для любого ввода или удаление данных не сохранили бы допустимые результаты. Чёрного списка символов тоже недостаточно: у настоящего владельца есть апостроф в имени. Задуманная защита — разделение фиксированного текста SQL и привязанных значений.

Повторно просканируйте исправленный исходный код

На этом шаге вы объедините результат статического анализа с реальным поведением поиска, которое только что проверили.

course-code-scan lookup.py

Ожидается No findings under the fixed local rule. Переданное имя больше не попадает в строку команды SQL. Оно передаётся как отдельное привязанное значение.

Повторно запустите функциональную проверку:

bash test.sh

Ожидается тот же успешный результат. Сканирование и проверка поведения отвечают на разные вопросы: одно исследует целевой шаблон кода; другая проверяет, что настоящие имена всё ещё работают, а специальный ввод не может расширить результат.

Проверка курса также наблюдает реальные аргументы SQLite, поэтому простое сокрытие находки сканера или ручная замена символов кавычек не является требуемым исправлением. Сохраните исходную базу данных, подготовленные тестовые средства, средство запуска и интерфейс функции. Письменный файл исследования не нужен.

Эта лабораторная работа обучала одному небольшому шаблону привязки SQLite и одному локальному правилу SAST. Полная проверка приложения также учитывала бы другие источники ввода и классы уязвимостей. Здесь вы устранили конкретный риск попадания ввода в запрос, сохранив задуманный поиск.

Резюме

Вы проследили переданное имя через короткую функцию поиска Python и условие SQL, связали реальную локальную находку Semgrep с поведением при выполнении и использовали привязку параметров SQLite. Обычные имена и имена с апострофами по-прежнему работают, а ввод, меняющий запрос, не может расширить результаты. Статические и функциональные подтверждения обосновывают исправление конкретной границы.