Обнаружение 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")