检测源代码中的SQL注入

时间:2014-12-10 13:37:38

标签: python sql security sql-injection static-code-analysis

请考虑以下代码段:

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注入攻击,因为查询不是通过DB API参数化的,而是通过字符串格式化构建的。如果你以这种方式调用函数:

get_data("'; DROP TABLE TEST -- ")

将执行以下查询:

SELECT * FROM TEST WHERE ID = ''; DROP TABLE TEST --  

现在,我的目标是分析项目中的代码并检测可能容易受到SQL注入的所有地方。换句话说,查询是通过字符串格式构造的,而不是在单独的参数中传递查询参数。

pylintpyflakes或任何其他静态代码分析包的帮助下,它是否可以静态解决?


我了解sqlmap流行的渗透测试工具,但据我所知,它正在对付网络资源,通过HTTP请求将其作为黑盒进行测试。

4 个答案:

答案 0 :(得分:11)

有一种工具可以尝试准确解决问题所在,py-find-injection

  

py_find_injection使用各种启发式方法来查找SQL注入   python源代码中的漏洞。

它使用ast module,查找session.execute()cursor.execute()次调用,并检查内部查询是否通过string interpolation, concatenation or format()形成。

以下是在检查问题中的代码段时输出的内容:

$ py-find-injection test.py
test.py:6   string interpolation of SQL query
1 total errors

但是,该项目未得到主动维护,但可以作为起点。一个好主意就是从中创建一个pylintpyflakes插件。

答案 1 :(得分:6)

不确定这将如何与其他包进行比较,但在某种程度上,您需要解析传递给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 )

您还可以使用scanString而不是searchString让pyparsing报告找到的行的位置。

答案 2 :(得分:1)

关于我认为你能得到的最好的东西是通过你的代码库进行grep'ing,寻找使用Python字符串插值传递字符串的cursor.execute()语句,如你的例子所示:

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)

但它可能是你可以轻松做到的最好的。

答案 3 :(得分:-1)

您已经意识到问题并尝试解决问题,这是一件好事。

您可能已经知道,在任何数据库中执行SQL的最佳实践是使用预准备语句或存储过程(如果可用)。

在这种特殊情况下,您可以通过“准备”语句然后执行来实现预准备语句。

e.g:

cursor = db.cursor()
query = "SELECT * FROM TEST WHERE ID = %s"  
cur.execute(query, "2")