16

在通过 exact 为 Django ORM order找到解决方案的过程中,我创建了一个自定义 django Func:

from django.db.models import Func

class Position(Func):
    function = 'POSITION'
    template = "%(function)s(LOWER('%(substring)s') in LOWER(%(expressions)s))"
    template_sqlite = "instr(lower(%(expressions)s), lower('%(substring)s'))"

    def __init__(self, expression, substring):
        super(Position, self).__init__(expression, substring=substring)

    def as_sqlite(self, compiler, connection):
        return self.as_sql(compiler, connection, template=self.template_sqlite)

其工作原理如下:

class A(models.Model):
    title = models.CharField(max_length=30)

data = ['Port 2', 'port 1', 'A port', 'Bport', 'Endport']
for title in data:
    A.objects.create(title=title)

search = 'port'
qs = A.objects.filter(
        title__icontains=search
    ).annotate(
        pos=Position('title', search)
    ).order_by('pos').values_list('title', flat=True)
# result is
# ['Port 2', 'port 1', 'Bport', 'A port', 'Endport'] 

但正如@hynekcer 评论的那样:

“它很容易崩溃,因为应用程序的') in '') from myapp_suburb; drop ... 名称是“myapp and autocommit is enabled”。

主要问题是额外数据 ( substring) 在没有 sqlescape 的情况下进入模板,这使应用程序容易受到 SQL 注入攻击。

我找不到 Django 的保护方法。


我创建了一个repo (djposfunc),您可以在其中测试任何解决方案。

4

3 回答 3

12

TL;DR: Django 文档中的 所有示例Func()都可以轻松地使用一个参数安全地实现其他类似的 SQL 函数。所有作为其后代的内置 Django数据库函数条件函数Func()在设计上也是安全的。超出此限制的申请需要评论。


Func()类是 Django 查询表达式中最通用的部分。它允许以某种方式将几乎任何函数或运算符实现到 Django ORM 中。它就像一把瑞士军刀,非常通用,但与使用专用工具(如带有光屏障的电动切割机)相比,必须更加注意不要割伤自己。如果一旦“升级”的“安全”小刀无法放入口袋,那么用铁锤用铁锤锻造自己的工具仍然要安全得多。


安全说明

  • Func(*expressions, **extra)应首先阅读带有示例的简短文档。(我在这里推荐 Django 2.0 的开发文档,其中最近添加了更多与您的示例完全相关的安全信息,包括避免 SQL 注入。)

  • 中的所有位置参数*expressions都由Django编译,即Value(string)移动到参数中,并由数据库驱动程序正确转义。

  • 其他字符串被解释为字段名称F(name),然后以右别名点为前缀table_name.,最终添加到该表的连接,并且名称由quote_name()函数处理。
  • 问题是 1.11 中的文档仍然很简单,诱人的参数**extra**extra_context模糊的文档。它们只能用于永远不会“编译”并且永远不会通过 SQL的简单参数params。没有撇号、反斜杠或百分号的安全字符的数字或简单字符串是好的。它不能是字段名,因为它不会明确,也不会加入。对于以前检查过的数字和固定字符串(如“ASC”/“DESC”、时区名称和其他值(如下拉列表))是安全的。还有一个弱点。必须在服务器端检查下拉列表值。还必须验证数字是数字,而不是数字字符串'2'因为所有数据库函数都默默地接受省略的数字字符串而不是数字。如果传递了错误的“数字”,'0) from my_app.my_table; rogue_sql; --'则注射结束。请注意,在这种情况下,流氓字符串不包含任何非常禁止的字符。必须专门检查用户提供的数字,否则该值必须通过 positional 传递expressions
  • 指定Func 类的function名称和字符串属性或Func() 调用的相同和参数是安全的。参数不应在括号内的替换参数表达式周围包含撇号:,因为如果需要,数据库驱动程序会添加撇号,但额外的撇号可能会导致它通常无法正常工作,但有时可能会被忽略,这会导致另一个安全问题arg_joinerfunctionarg_joinertemplate( %(expressions)s )

与安全无关的注意事项

  • 许多带有一个参数的简单内置函数看起来并不简单,因为它们派生自 Func 的多用途后代。例如Length,一个函数也可以用作 lookup Transform

    class Length(Transform):
        """Return the number of characters in the expression."""
        function = 'LENGTH'
        output_field = fields.IntegerField()  # sometimes specified the type
        # lookup_name = 'length'  # useful for lookup not for Func usage
    

    查找转换将相同的功能应用于查找的左侧和右侧。

    # I'm searching people with usernames longer than mine 
    qs = User.objects.filter(username__length__gt=my_username)
    
  • Func.as_sql(..., function=..., template=..., arg_joiner=...)如果没有在自定义 as_sql() 中覆盖,则可以在其中指定相同的关键字参数,Func.__init__()或者可以将它们设置为自定义后代类的属性Func

  • 许多 SQL 数据库函数都有一个冗长的语法,POSITION(substring IN string)因为如果不支持命名参数,它会简化可读性,比如POSITION($1 IN $2)一个简短的变体STRPOS(string, substring)(por postgres)或INSTR(string, substring)(对于其他数据库)更容易实现,Func()并且可读性由 Python 包装器用__init__(expression, substring).

  • 也可以通过将更多嵌套函数与简单参数安全的方式组合来实现非常复杂的函数:Case(When(field_name=lookup_value, then=Value(value)), When(...),... default=Value(value)).

于 2017-11-04T20:59:04.787 回答
6

通常,使您容易受到 SQL 注入攻击的是“流浪”单引号'
单引号对之间包含的所有内容都将按应有的方式处理,但未配对的单引号可能会结束字符串并允许条目的其余部分充当可执行代码。
@hynekcer 的示例正是这种情况。

Django提供了Value防止上述情况的方法:

该值将被添加到 SQL 参数列表中并被正确引用

因此,如果您确保通过该Value方法传递每个用户输入,您将没问题:

from django.db.models import Value

search = user_input
qs = A.objects.filter(title__icontains=search)
              .annotate(pos=Position('title', Value(search)))
              .order_by('pos').values_list('title', flat=True)

编辑:

如评论中所述,在上述设置中似乎没有按预期工作。但是,如果调用如下,则它可以工作:

pos=Func(F('title'), Value(search), function='INSTR')

作为旁注:为什么首先要弄乱模板?

您可以从任何数据库语言(例如:SQLite、PostgreSQL、MySQL 等)中找到您想要使用的函数并明确使用它:

class Position(Func):
    function = 'POSITION' # MySQL default in your example

    def as_sqlite(self, compiler, connection):
        return self.as_sql(compiler, connection, function='INSTR')

    def as_postgresql(self, compiler, connection):
        return self.as_sql(compiler, connection, function='STRPOS')

    ...

编辑:

您可以在调用中使用其他函数(如LOWER函数)Func,如下所示:

pos=Func(Lower(F('title')), Lower(Value(search)), function='INSTR')
于 2017-09-29T14:26:50.300 回答
5

基于 John Moutafis 的想法,最终函数是(在__init__我们Values用于安全结果的方法中。)

from django.db.models import Func, F, Value
from django.db.models.functions import Lower


class Instr(Func):
    function = 'INSTR'

    def __init__(self, string, substring, insensitive=False, **extra):
        if not substring:
            raise ValueError('Empty substring not allowed')
        if not insensitive:
            expressions = F(string), Value(substring)
        else:
            expressions = Lower(string), Lower(Value(substring))
        super(Instr, self).__init__(*expressions)

    def as_postgresql(self, compiler, connection):
        return self.as_sql(compiler, connection, function='STRPOS')
于 2017-09-29T23:36:53.003 回答