Django自定义复杂的Func(sql函数)

Bea*_*own 13 python django django-queryset django-annotate

在为精确找到Django ORM顺序的解决方案的过程中,我创建了一个自定义的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)
Run Code Online (Sandbox Code Playgroud)

其工作原理如下:

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'] 
Run Code Online (Sandbox Code Playgroud)

但正如@hynekcer评论的那样:

" 由于 预计应用程序的名称是"myapp和autocommit已启用",因此很容易崩溃') in '') from myapp_suburb; drop ....

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

我找不到哪种方法可以保护Django.


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

Joh*_*fis 5

通常,让您容易受到 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)
Run Code Online (Sandbox Code Playgroud)

编辑:

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

pos=Func(F('title'), Value(search), function='INSTR')
Run Code Online (Sandbox Code Playgroud)

附带说明:为什么首先要弄乱模板?

您可以从任何数据库语言(例如: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')

    ...
Run Code Online (Sandbox Code Playgroud)

编辑:

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

pos=Func(Lower(F('title')), Lower(Value(search)), function='INSTR')
Run Code Online (Sandbox Code Playgroud)


小智 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')
Run Code Online (Sandbox Code Playgroud)


hyn*_*cer 5

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


Func()类是Django Query表达式中最通用的部分。它允许以某种方式在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; --'则注入结束。请注意,在这种情况下,流氓字符串不包含任何非常禁止的字符。用户提供的数字必须经过专门检查,或者值必须通过position传递expressions
  • 它是安全的指定function名称和arg_joiner函数功能类的字符串的属性或相同functionarg_joinerFunc键()调用的参数。该template参数绝不能在括号内的替换参数表达式周围包含撇号:( %(expressions)s ),因为数据库驱动程序会在必要时添加撇号,但是附加的撇号可能会导致其通常无法正常工作,但有时可能会被忽略,从而导致另一个安全问题

与安全无关的注释

  • 许多带有一个参数的简单内置函数看起来并不那么简单,因为它们是从Func的多用途后代派生而来的。例如Length,一个函数也可以用作查找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
    
    Run Code Online (Sandbox Code Playgroud)

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

    # I'm searching people with usernames longer than mine 
    qs = User.objects.filter(username__length__gt=my_username)
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果未在自定义as_sql()中覆盖,Func.as_sql(..., function=..., template=..., arg_joiner=...)则可以已经在其中指定了可以在中指定的相同关键字参数,也可以将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))