Django custom для сложной функции 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:

"это падает легко by ') in '') from myapp_suburb; drop ... ожидается, что имя приложения " myapp и autocommit включен."

главная проблема заключается в том, что дополнительные данные (substring) попал в шаблон без sqlescape, который оставляет приложение уязвимым для атак SQL-инъекций.

Я не могу найти, какой способ Django защитить от этого.


Я создал РЕПО (djposfunc) где вы можете проверить любое решение.

3 ответов


TL; DR: Все примеры с Func() в Django docs можно легко использовать для безопасной реализации других подобных функций SQL с одним аргументом. Все встроенные в Django функции базы данных и условные функции это потомки Func() также безопасны по дизайну. Приложение не нуждается в комментариях.


класс кнопку func() - самый общая часть выражений запросов Django. Это позволяет реализовать практически любую функцию или оператор в Django ORM каким-то образом. Это как швейцарский армейский нож, очень универсальный, но нужно быть немного более внимательным, чтобы не порезать себя, чем специализированным инструментом (например, электрическим резцом с оптическим барьером). Это все еще гораздо более безопасно, чем подделать собственный инструмент молотком из куска железа, если когда-то "модернизированный" "безопасный" карманный нож не вписывается карман.


примечания по безопасности

  • краткая документация для Func(*expressions, **extra) с примерами следует ознакомиться в первую очередь. (Я рекомендую здесь документы разработки для Django 2.0, где недавно добавлена дополнительная информация о безопасности, включая избегая SQL инъекции, связанные именно с вашим примером.)

  • все позиционные аргументы в *expressions несколько составлен Джанго, то есть Value(string) перемещаются в Параметры, где они правильно экранируются драйвером базы данных.

  • другие строки интерпретируются как имена полей F(name), затем с префиксом справа table_name. alias dot, в конечном итоге добавляется соединение с этой таблицей, а имена обрабатываются

на основе идей Джона 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')

обычно то, что оставляет вас уязвимыми для атаки 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)

EDIT:

как указано в комментариях, это, похоже, не работает так, как ожидалось в приведенной выше настройке. Но если вызов следующий, он работает:

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

    ...

EDIT:

вы можете использовать другие функции (например,