如何使用“SELECT * FROM {table_name}”避免SQL注入?

时间:2016-05-19 16:43:33

标签: python sql postgresql sql-injection psycopg2

在使用Psycopg2的Python中使用以下代码:

import psycopg2

import getpass

conn = psycopg2.connect("dbname=mydb user=%s" % getpass.getuser())
cursor = conn.cursor()

tables = ["user", "group", "partner", "product"]
for table in tables:

    # with sql injection
    cursor.execute("SELECT name FROM %s LIMIT 1" % (table,))
    print "table", table, "result", len(cursor.fetchone())

    # without sql injection
    cursor.execute("SELECT name FROM %s LIMIT 1", (table,))
    print "table", table, "result", len(cursor.fetchone())

输出结果为:

table res_partner result 1
Traceback (most recent call last):
  File "my_psycopg2_example.py", line 16, in <module>
    cursor.execute("SELECT name FROM %s LIMIT 1", (table,))
psycopg2.ProgrammingError: syntax error at or near "'res_partner'"
LINE 1: SELECT name FROM 'res_partner' LIMIT 1

使用SQL注入它可以正常工作。

但我们不想创建安全问题。

我们阅读了this documentation,并在其中找到了以下评论:

  

只应通过此方法绑定变量值:它不应用于设置表或字段名称。对于这些元素,应在运行execute()之前使用普通的字符串格式。

但是如果我们使用“普通字符串格式化”,我们也会使用SQL注入。

管理这种特殊情况的好方法是什么,避免SQL注入?

4 个答案:

答案 0 :(得分:3)

我认为你混淆了SQL注入的定义。 SQL注入是您软件上的攻击,有人会让您的SQL查询执行您不想要的操作。字符串插值不是SQL注入。字符串插值有时可以启用SQL注入,但并非总是如此。要查看字符串插值并不总是不安全,请考虑以下哪项是最安全的:

  1. sql = 'SELECT name FROM user'
  2. sql = 'SELECT name FROM ' + 'user'
  3. sql = 'SELECT name FROM %s' % ['user']
  4. sql = 'SELECT name FROM {}'.format('user')
  5. 这些代码行中的每一行都完全相同,因此它们中没有一个比其他代码更安全或更不安全。在您的确切示例中,没有SQL注入的危险,因为您只是构建一个硬编码的SQL查询字符串。

    另一方面,如果您的table值来自用户,则可能存在安全问题:

    • 如果他们传递了一个存在的表的名称,但您不希望他们查询该怎么办?

      table = 'secrets'
      sql = 'SELECT name FROM %s LIMIT 1' % table
      

      结果:

      SELECT name FROM secrets LIMIT 1
      
    • 如果他们传递的something实际上不是表名怎么办?

      table = 'product; DROP TABLE user; --'
      sql = 'SELECT name FROM %s LIMIT 1' % table
      

      结果:

      SELECT name FROM product;
      DROP TABLE user;
      -- LIMIT 1
      

    您可以通过检查是否允许表名来阻止这种情况:

    if table.lower() not in ["user", "group", "partner", "product"]:
        raise Something('Bad table name: %r' % table)
    

答案 1 :(得分:0)

execute函数中使用psycopg2查询参数是最安全的,并且在将参数用作文字时很容易使用。

cursor.mogrify("select * from foo where bar = %s", ('example',))
# yields "select * from foo where bar = 'example'"

(请注意, cursor.mogrify() 的行为类似于执行,但只显示格式化的SQL而不实际执行它。

但是,当您希望参数为表,模式或其他标识符时,这样做有点棘手。您可以使用AsIs来包装您的参数,但这仍然为SQL注入打开了大门。

from psycopg2.extensions import AsIs

cur.mogrify('select %s from foo;', (AsIs('* from dual; drop table students; --'),))
# yields 'select * from dual; drop table students; -- from foo;'

看起来psycopg2(&gt; = 2.7)的新开发将有一个Identifier类,您可以将参数包装进来并希望是安全的。如果它还没有发布,或者你没有它,那么这是一种创建自己的类的方法。我将在下面给出一些片段,但您也可以看到my gist

import re
import psycopg2.extensions

class NotSqlIdentifierError(Exception):
    pass

valid_pattern = r'^[a-zA-Z_][a-zA-Z0-9_\$]*$'

class QuotedIdentifier(object):
    def __init__(self, obj_str):
        self.obj_str = obj_str

    def getquoted(self):
        if re.match(valid_pattern, self.obj_str):
            return self.obj_str
        else:
            raise NotSqlIdentifierError(repr(self.obj_str))

psycopg2.extensions.register_adapter(QuotedIdentifier, lambda x: x)

如果你已经有一个psycopg2游标实例,你可以这样测试/使用它:

# Test that a valid identifier formats into string
cursor.mogrify('select %s from foo;', (QuotedIdentifier('bar'),))
# returns 'select bar from foo;'

# Test formatting both an identifier and a literal
cursor.mogrify(
    'select * from foo where %s = %s;', 
    (
        QuotedIdentifier('bar'),
        'example'
    )
)
# returns "select * from foo where bar = 'example';"

# Test that a non-valid identifier fails with exception
cursor.mogrify('select %s from foo;', (QuotedIdentifier('* from dummy; drop table students; --'),))
"""Returns following:
---------------------------------------------------------------------------
NotSqlIdentifierError                     Traceback (most recent call last)
<ipython-input-14-d6a960dc458a> in <module>()
----> 1 cur.mogrify('select %s from foo;', (QuotedIdentifier('* from dummy; drop table students; --'),))
<ipython-input-12-0a1327cbaf78> in getquoted(self)
     18             return self.obj_str
     19         else:
---> 20             raise NotSqlIdentifierError(repr(self.obj_str))
     21 
     22 psycopg2.extensions.register_adapter(QuotedIdentifier, lambda x: x)
NotSqlIdentifierError: '* from dummy; drop table students; --'
"""

有关自定义类包装SQL参数的机制的更多信息,请参阅文档中的this section

答案 2 :(得分:0)

我认为您的代码可能只是缺少“;”。您的一行可能如下所示:

cursor.execute('SELECT name FROM %s LIMIT 1;', (table,))

如果所有其他方法都失败了,您可以为该输入设置一个过滤掉控制字符的过滤器。对我来说听起来很痛苦,但可以做到。然后你的代码看起来像:

cursor.execute('SELECT name FROM %s LIMIT 1;' % (functionToSanitizeInput(table)))

也许你可以使用这个项目:
bleach.readthedocs.io

=============================================== =

非常好参考: http://bobby-tables.com/

=============================================== =

答案 3 :(得分:0)

这是一篇有关注入和python代码的不错的文章 https://realpython.com/prevent-python-sql-injection/

您可以变换

tables = ["user", "group", "partner", "product"]
for table in tables:    
    cursor.execute("SELECT name FROM %s LIMIT 1" % (table,))

from psycopg2 import sql

tables = ["user", "group", "partner", "product"]
for table_name in tables:        
    stmt = sql.SQL("SELECT name FROM {table_name} LIMIT 1").format(
        table_name = sql.Identifier(table_name),
    )
    cursor.execute(stmt)