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

Django custom for complex Func (sql function)

在为 寻找解决方案的过程中,我创建了一个自定义的 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 评论的那样:

"It crashes easily by ') in '') from myapp_suburb; drop ... expected that the name of the app is "myapp and autocommit is enabled."

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

我找不到 Django 的保护方式。


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

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

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

The value will be added into the SQL parameter list and properly quoted.

因此,如果您确保通过 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')

    ...

编辑:

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

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

基于 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')

TL;DR: Django 文档中所有带有 Func() 的示例都可以很容易地用于安全地使用一个参数实现其他类似的 SQL 函数。 所有作为 Func() 后代的内置 Django database fuctions and conditional functions 在设计上也是安全的。超出此限制的应用需要评论。


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


安全说明

  • Func(*expressions, **extra) with examples should be read first. (I recommend here the development docs for Django 2.0 where is recently added more security information, including Avoiding SQL injection 的简短文档,与您的示例完全相关。)

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

  • 其他字符串被解释为字段名称 F(name),然后以右 table_name. 别名点为前缀,最后添加到 table 的连接,名称由 [=19] 处理=]函数。
  • 问题是1.11的文档还是很简单,诱人的参数**extra**extra_context 被模糊地记录下来。它们只能用于 永远不会 "compiled" 并且永远不会经过 SQL params 的简单参数。没有撇号、反斜杠或百分比的带有安全字符的数字或简单字符串是好的。不能是字段名,因为不会有歧义,也不会加入。对于之前检查过的数字和固定字符串(如 'ASC'/'DESC'、时区名称和其他值(如下拉列表中的值),它是安全的。仍然有一个弱点。必须在服务器端检查下拉列表值。还必须验证数字是数字,而不是像 '2' 这样的数字字符串,因为所有数据库函数都默默地接受省略的数字字符串而不是数字。如果传递了 false "number" '0) from my_app.my_table; rogue_sql; --' 则注入结束。请注意,在这种情况下,流氓字符串不包含任何非常禁止的字符。必须专门检查用户提供的数字,或者必须通过位置 expressions.
  • 传递值
  • 可以安全地指定 Func function 名称和 arg_joiner 字符串属性 class 或 Func() 的相同 functionarg_joiner 参数称呼。 template 参数永远不应在括号内的替换参数表达式周围包含撇号:( %(expressions)s ),因为数据库驱动程序会在必要时添加撇号,但额外的撇号可能会导致它通常不能正常工作,但有时它可能会被忽略,这会导致 another security issue.

与安全无关的注意事项

  • 许多只有一个参数的简单内置函数看起来并不尽可能简单,因为它们派生自 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
    

    查找转换对查找的左侧和右侧应用相同的函数。

    # 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=...) 中指定的相同关键字参数可以在 Func.__init__() 中指定,如果不在自定义 as_sql() 中覆盖,或者它们可以是设置为 Func.

  • 的自定义后代 class 的属性
  • 许多 SQL 数据库函数具有像 POSITION(substring IN string) 这样冗长的语法,因为如果不支持命名参数(如 POSITION( IN ) 和简短变体 [=],它会简化可读性41=] (por postgres) 或 INSTR(string, substring) (对于其他数据库) 由 Func() 更容易实现,可读性由 Python 包装器和 __init__(expression, substring).

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