web-dev-qa-db-ja.com

Django複雑なFuncのカスタム(SQL関数)

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)

これは次のように機能します。

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) を作成しました。ここで、任意のソリューションをテストできます。

14
Bear Brown

TL; DR:Func() を含むすべての例Django docsを使用すると、1つの引数で他の同様のSQL関数を安全に実装できます。すべての組み込みDjango database fuctions および 条件付き関数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; --'_が注入されます。この場合、不正な文字列には非常に禁止的な文字が含まれていないことに注意してください。ユーザー指定の数値を具体的に確認するか、値を位置expressionsを介して渡す必要があります。
  • Funcクラスのfunction nameおよび_arg_joiner_文字列属性、またはFunc()呼び出しの同じfunctionおよび_arg_joiner_パラメータを指定しても安全です。 templateパラメーターには、括弧内の置換されたパラメーター式の前後にアポストロフィを含めないでください:_(_ %(expressions)s _)_。通常は正しく機能しませんが、見落とされて 別のセキュリティ問題 につながる可能性があります。

セキュリティに関連しない注意事項

  • 1つの引数を持つ多くの単純な組み込み関数は、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=...)で指定できる同じキーワード引数は、カスタムのas_sql()で上書きされない場合、Func.__init__()ですでに指定できます。または、Funcのカスタムの子孫クラスの属性として設定できます。 。

  • 多くのSQLデータベース関数はPOSITION(substring IN string)のような詳細な構文を持っています。これは、名前付きパラメーターがPOSITION($1 IN $2)や簡単なバリアントSTRPOS(string, substring)(postgres)またはINSTR(string, substring)(他のデータベースの場合)Func()で簡単に実装でき、読みやすさはPython __init__(expression, substring)

  • また、非常に複雑な関数は、ネストされた関数と単純な引数の安全な方法を組み合わせて実装することもできます:Case(When(field_name=lookup_value, then=Value(value)), When(...),... default=Value(value))

7
hynekcer

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')
4
Bear Brown

通常、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')
3
John Moutafis