2017-09-26 59 views
8

在找到一个解决方案,Django ORM order by exact的过程中,我创建自定义DJANGO FUNC:Django的自定义为复杂的函数功能(SQL函数)

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并自动提交已启用。”

的主要问题是,额外的数据(substring)钻进模板而不sqlescape这让应用程序容易受到SQL注入式攻击。

我无法找到哪个是Django的防护方法。


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

+0

对不起,我在本地回答了这个安全问题,然后等待解决问题。现在我写了对原始问题的正常答案。 – hynekcer

回答

2

TL; DR: 在Django文档Func()所有的例子都可以很容易地用于安全地实现其他类似的SQL函数有一个参数。 的Func()所有内置的Django database fuctionsconditional functions是后人也通过设计安全。超出此限制的应用需要评论。


Func()是Django的查询表达式的最普遍的部分。它允许以某种方式将几乎任何函数或操作符实现为Django ORM。它像像瑞士军刀,非常普遍,但一个人必须更专注于不切自己,比专业工具(如电动切割机与光学屏障)。如果一个“升级的”“安全”的小刀不能放进口袋里,那么用锤子锻造一个自己的工具还是要安全得多。


安全注意事项

  • Func(*expressions, **extra)举例短文件应先阅读。 (我推荐这里的Django 2.0的开发文档,其中最近添加了更多的安全信息,包括Avoiding SQL injection,正好与你的例子。)

  • 所有位置参数在*expressionsDjango在编译,即Value(string)被移动到参数,在那里它们被数据库驱动程序正确转义。

  • 其他字符串被解释为字段名称F(name),然后以table_name. alias为前缀,最后会添加到该表的连接,并且名称被quote_name()函数处理。
  • 的问题是,在1.11的文档仍然是简单的,诱人的参数**extra**extra_context都依稀记载。它们只能用于简单的参数,这些参数将是从未“编译”的并且从未经过SQL params。数字或简单字符串与安全字符无撇号,反斜杠或百分比是好的。它不能是一个字段名称,因为它不会是明确的,也不会加入。对于以前检查的数字和固定字符串(如“ASC”/“DESC”),时区名称和其他值(例如从下拉列表)中是安全的。还有一个弱点。下拉列表值必须在服务器端进行检查。数字还必须验证他们是数字,而不是一个数字串像'2',因为所有的数据库功能默默接受的省略数字字符串,而不是数量。如果通过一个错误的“数字”'0) from my_app.my_table; rogue_sql; --'那么注射结束。请注意,在这种情况下,流氓字符串不包含任何非常禁止的字符。用户提供的号码必须特别检查,或者该值必须通过位置expressions
  • 指定function名称和arg_joiner Func类的字符串属性或与Func()调用的参数相同的functionarg_joiner是安全的。 template参数决不能在括号内的替代参数表达式中包含撇号:(%(expressions)s),因为如果需要,数据库驱动程序会添加撇号,但是额外的撇号可能导致它通常无法正常工作,但有时可能会忽略它,那会导致another security issue

注意事项不涉及安全性的一个参数就

  • 许多简单的内置函数不看尽可能简单,因为它们是从函数功能的多功能后代的。例如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=...)指定相同的关键字参数可如果定制as_sql(),也可以设置为自定义子类的Func的属性不会覆盖已经被指定在Func.__init__()

  • 许多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))

2

通常情况下,您容易遭受SQL注入攻击的原因是the "stray" single quotes '
单引号对之间的所有内容都将按照原样处理,但未配对的单引号可能会结束字符串并允许其余条目充当可执行代码片段。
@ 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') 

    ... 

编辑:

您可以使用一个Func呼叫内的其它功能(如LOWER函数)如下:

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

你是否尝试过你的解决方案,在我的测试中它不起作用。 关于SQL函数,它不适用于当前需要不区分大小写。 '为什么会搞成与其他我能方式,第一place'模板表示,需要为复杂的功能 –

+0

解决方案,我在你的代码@BearBrown运行一些测试,如果我运行'POS = Func键(F(“标题” ),价值(搜索),函数= 'INSTR')',而不是'位置( '标题',搜索)'我通过了测试,但如果我这样做'POS =位置( '标题',值(搜索))'它确实失败。也许一些Django的bug? –

+0

感谢您的详细信息我会再次看到 –

2

基础在约翰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') 
+0

很好@BearBrown :) –