2017-09-26 58 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 :) –