Обнаружение SQL-инъекций в исходном коде
Рассмотрим следующий фрагмент кода:
import MySQLdb
def get_data(id):
db = MySQLdb.connect(db='TEST')
cursor = db.cursor()
cursor.execute("SELECT * FROM TEST WHERE ID = '%s'" % id)
return cursor.fetchall()
print(get_data(1))
В коде есть серьезная проблема - он уязвим для атак SQL-инъекций, поскольку запрос не параметризируется через API БД и построен с помощью форматирования строк. Если вы вызываете функцию следующим образом:
get_data("'; DROP TABLE TEST -- ")
будет выполнен следующий запрос:
SELECT * FROM TEST WHERE ID = ''; DROP TABLE TEST --
Теперь моя цель - проанализировать код в проекте и обнаружить все места, потенциально уязвимые для инъекций SQL. Другими словами, когда запрос строится с помощью форматирования строк, в отличие от передачи параметров запроса в отдельном аргументе.
Это то, что можно решить статически, с помощью pylint
, pyflakes
или любых других пакетов статического анализа кода?
Я знаю sqlmap
популярный инструмент тестирования проникновения, но, насколько я понимаю, он работает против веб-ресурса, тестируя его как черный ящик через HTTP-запросы.
Ответы
Ответ 1
Есть инструмент, который пытается точно решить, о чем идет речь, py-find-injection
:
py_find_injection использует различные эвристики для поиска SQL-инъекций уязвимостей в исходном коде python.
Он использует ast
module, ищет вызовы session.execute()
и cursor.execute()
и проверяет, формируется ли запрос внутри через строка интерполяции, конкатенации или format()
.
Вот что он выводит при проверке фрагмента в вопросе:
$ py-find-injection test.py
test.py:6 string interpolation of SQL query
1 total errors
Проект, однако, активно не поддерживается, но может использоваться в качестве отправной точки. Хорошей идеей было бы сделать плагин pylint
или pyflakes
.
Ответ 2
Не уверен, как это будет сравниваться с другими пакетами, но в определенной степени вам нужно проанализировать аргументы, передаваемые cursor.execute
. Этот бит pyparsing-кода ищет:
-
аргументы с использованием строковой интерполяции
-
с использованием конкатенации строк с именами переменных
-
аргументы, которые являются только именами переменных
Но иногда аргументы используют конкатенацию строк только для того, чтобы разбить длинную строку на: если все строки в выражении объединяются в литералы, нет никакого риска внедрения SQL.
Этот фрагмент pyparsing будет искать вызовы cursor.execute, а затем искать формы аргументов риска:
from pyparsing import *
import re
identifier = Word(alphas, alphanums+'_')
integer = Word(nums)
LPAR,RPAR,PLUS,PERCENT = map(Literal, '()+%')
stringInterpRE = re.compile(r"%-?\d*\*?\.?\d*\*?s")
def containsStringInterpolation(s,l,tokens):
if not stringInterpRE.search(tokens[0]):
raise ParseException(s,l,"No string interpolation")
tupleContents = identifier | integer
tupleExpr = LPAR + delimitedList(tupleContents) + RPAR
stringInterpArg = identifier | tupleExpr
interpolatedString = originalTextFor(quotedString.copy().setParseAction(containsStringInterpolation) +
PERCENT + stringInterpArg)
stringTerm = interpolatedString | OneOrMore(quotedString.copy()) | identifier
stringTerm.setName("stringTerm")
unsafeStringExpr = (stringTerm + OneOrMore(PLUS + stringTerm)) | identifier | interpolatedString
def unsafeExpr(s,l,tokens):
if not any(term == interpolatedString or term == identifier
for term in tokens):
raise ParseException(s,l,"No unsafe string terms")
unsafeStringExpr.setParseAction(unsafeExpr)
unsafeStringExpr.setName("unsafeExpr")
func = Literal("cursor.execute")
statement = func + LPAR + unsafeStringExpr + RPAR
statement.setName("execute stmt")
#statement.ignore(pythonComment)
for tokens in statement.searchString(sample):
print ' '.join(tokens.asList())
Это сканирует следующий пример:
sample = """
import MySQLdb
def get_data(id):
db = MySQLdb.connect(db='TEST')
cursor = db.cursor()
cursor.execute("SELECT * FROM TEST WHERE ID = '%s' -- UNSAFE" % id)
cursor.execute("SELECT * FROM TEST WHERE ID = '" + id + "' -- UNSAFE")
cursor.execute(sqlVar + " -- UNSAFE")
cursor.execute("SELECT * FROM TEST WHERE ID = 'FRED' -- SAFE")
cursor.execute("SELECT * FROM TEST WHERE ID = " +
"'FRED' -- SAFE")
cursor.execute("SELECT * FROM TEST "
"WHERE ID = "
"'FRED' -- SAFE")
cursor.execute("SELECT * FROM TEST "
"WHERE ID = " +
"'%s' -- UNSAFE" % name)
return cursor.fetchall()
print(get_data(1))"""
и сообщите об этих небезопасных заявлениях:
cursor.execute ( "SELECT * FROM TEST WHERE ID = '%s' -- UNSAFE" % id )
cursor.execute ( "SELECT * FROM TEST WHERE ID = '" + id + "' -- UNSAFE" )
cursor.execute ( sqlVar + " -- UNSAFE" )
cursor.execute ( "SELECT * FROM TEST " "WHERE ID = " + "'%s' -- UNSAFE" % name )
Вы также можете указать pyparsing о местоположении найденных строк, используя scanString вместо searchString.
Ответ 3
О лучшем, что я могу думать, что вы получите, будет grep'ing через вашу кодовую базу, ища инструкции cursor.execute(), передаваемые строкой с использованием интерполяции строк Python, как в вашем примере:
cursor.execute("SELECT * FROM TEST WHERE ID = '%s'" % id)
который, конечно, должен был быть написан как параметризованный запрос, чтобы избежать этой уязвимости:
cursor.execute("SELECT * FROM TEST WHERE ID = '%s'", (id,))
Это не будет идеальным - например, у вас может быть сложный код, например:
query = "SELECT * FROM TEST WHERE ID = '%s'" % id
# some stuff
cursor.execute(query)
Но это может быть самое лучшее, что вы можете легко сделать.
Ответ 4
Хорошо, что вы уже знаете о проблеме и пытаетесь ее решить.
Как вы уже знаете, наилучшие методы выполнения SQL в любой БД - это использование подготовленных операторов или хранимых процедур, если они доступны.
В этом конкретном случае вы можете реализовать подготовленный оператор, "подготовив" инструкцию и затем выполнив.
например:
cursor = db.cursor()
query = "SELECT * FROM TEST WHERE ID = %s"
cur.execute(query, "2")