web-dev-qa-db-fra.com

Django personnalisé pour Func complexe (fonction sql)

En train de trouver une solution pour ordre Django ORM par exact , j'ai créé un 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)

qui fonctionne comme suit:

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

Mais comme l'a commenté @hynekcer:

"Il se bloque facilement par ') in '') from myapp_suburb; drop ... s'attendait à ce que le nom de l'application soit "myapp et la validation automatique est activée".

Le principal problème est que des données supplémentaires (substring) sont entrées dans le modèle sans sqlescape, ce qui rend l'application vulnérable aux attaques par injection SQL.

Je ne trouve pas quelle est la manière Django de se protéger contre cela.


J'ai créé un repo (djposfunc) où vous pouvez tester n'importe quelle solution.

14
Bear Brown

TL; DR: Tous les exemples avec Func() = in Django peuvent être facilement utilisés pour implémenter en toute sécurité d'autres fonctions SQL similaires avec un seul argument. Tous les éléments intégrés Django bases de données et fonctions conditionnelles qui sont des descendants de Func() sont également sûrs par conception. L'application au-delà de cette limite doit être commentée.


La classe Func () est la partie la plus générale de Django Expressions de requête. Elle permet d'implémenter presque n'importe quelle fonction ou opérateur dans Django ORM d'une manière ou d'une autre. C'est comme un couteau suisse , très universel, mais il faut être un peu plus attentif à ne pas se couper, qu'avec un outil spécialisé (comme un cutter électrique à barrière optique) .C'est encore beaucoup plus sûr que de forger son propre outil au marteau à partir d'un morceau de fer, si une fois un couteau de poche "amélioré" "sécurisé" ne rentre pas dans la poche.


Notes de sécurité

  • La courte documentation pour Func(*expressions, **extra) avec des exemples doit être lue en premier. (Je recommande ici les documents de développement pour Django 2.0 où ont récemment été ajoutés plus d'informations de sécurité, y compris Éviter l'injection SQL , liées exactement à votre exemple.)

  • Tous les arguments positionnels dans *expressions sont compilés par Django, c'est-à-dire Value(string) sont déplacés vers les paramètres, où ils sont correctement échappés par le pilote de base de données.

  • Les autres chaînes sont interprétées comme des noms de champ F(name), puis préfixées par le point d'alias right table_name., éventuellement une jointure à cette table est ajoutée et les noms sont traités par la fonction quote_name().
  • Le problème est que la documentation en 1.11 est toujours simple, les paramètres séduisants **extra et **extra_context sont vaguement documentés. Ils ne peuvent être utilisés que pour des paramètres simples qui ne seront jamais "compilés" et ne passeront jamais par SQL params. Les nombres ou les chaînes simples avec des caractères sûrs sans apostrophe, barre oblique inverse ou pourcentage sont bons. Il ne peut pas s'agir d'un nom de champ, car il ne sera ni ambigu, ni joint. Il est sûr pour les nombres précédemment vérifiés et les chaînes fixes comme 'ASC'/'DESC', les noms de fuseau horaire et d'autres valeurs comme dans une liste déroulante. Il y a encore un point faible. Les valeurs des listes déroulantes doivent être vérifiées côté serveur. Il faut également vérifier que les nombres sont des nombres, pas une chaîne numérique comme '2' car toutes les fonctions de la base de données acceptent silencieusement une chaîne numérique omise au lieu de nombre. Si un faux "nombre" est passé '0) from my_app.my_table; rogue_sql; --' alors l'injection est terminée. Notez que la chaîne escroc ne contient aucun caractère très prohibitif dans ce cas. Les numéros fournis par l'utilisateur doivent être vérifiés spécifiquement ou la valeur doit être transmise via la position expressions.
  • Il est prudent de spécifier le nom function et les attributs de chaîne arg_joiner de la classe Func ou les mêmes paramètres function et arg_joiner de l'appel Func (). Le paramètre template ne doit jamais contenir d'apostrophes autour d'expressions de paramètre substituées entre parenthèses: (%(expressions)s), car des apostrophes sont ajoutées par le pilote de base de données si nécessaire, mais des apostrophes supplémentaires peuvent en résulter généralement ne fonctionne pas correctement, mais parfois il pourrait être ignoré et cela conduirait à n autre problème de sécurité .

Notes non liées à la sécurité

  • De nombreuses fonctions intégrées simples avec un seul argument ne semblent pas aussi simples que possible car elles sont dérivées de descendants polyvalents de Func. Par exemple, Length est une fonction qui peut également être utilisée comme recherche 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
    

    La transformation de recherche applique la même fonction aux côtés gauche et droit de la recherche.

    # I'm searching people with usernames longer than mine 
    qs = User.objects.filter(username__length__gt=my_username)
    
  • Les mêmes arguments de mot clé qui peuvent être spécifiés dans Func.as_sql(..., function=..., template=..., arg_joiner=...) peuvent déjà être spécifiés dans Func.__init__() s'ils ne sont pas remplacés dans as_sql () personnalisé ou ils peuvent être définis comme attributs d'une classe descendante personnalisée de Func.

  • De nombreuses fonctions de base de données SQL ont une syntaxe détaillée comme POSITION(substring IN string) car elle simplifie la lisibilité si les paramètres nommés ne sont pas pris en charge comme POSITION($1 IN $2) et une brève variante STRPOS(string, substring) (por postgres) ou INSTR(string, substring) (pour d'autres bases de données) qui est plus facile à mettre en œuvre par Func() et la lisibilité est fixée par le Python avec __init__(expression, substring).

  • Des fonctions très compliquées peuvent également être implémentées par une combinaison de fonctions plus imbriquées avec des arguments simples de manière sûre: Case(When(field_name=lookup_value, then=Value(value)), When(...),... default=Value(value)).

7
hynekcer

sur la base des idées de John Moutafis, la fonction finale est (à l'intérieur du __init__ méthode que nous utilisons Values pour le résultat de sécurité.)

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

Habituellement, ce qui vous rend vulnérable à une attaque par injection SQL est les guillemets simples "parasites" ' .
.
C'est exactement le cas sur l'exemple de @ hynekcer.

Django fournit la méthode Value pour éviter ce qui précède:

La valeur sera ajoutée dans la liste des paramètres SQL et correctement citée .

Donc, si vous vous assurez de passer chaque entrée utilisateur via la méthode Value, tout ira bien:

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)

MODIFIER:

Comme indiqué dans les commentaires, cela ne semble pas fonctionner comme prévu dans le cadre ci-dessus. Mais si l'appel est le suivant, cela fonctionne:

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

En remarque: Pourquoi jouer avec les modèles en premier lieu?

Vous pouvez trouver la fonction que vous souhaitez utiliser dans n'importe quel langage de base de données (ex: SQLite, PostgreSQL, MySQL, etc.) et l'utiliser explicitement:

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

    ...

MODIFIER:

Vous pouvez utiliser d'autres fonctions (comme la fonction LOWER) dans un appel Func comme suit:

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