python.django.security.audit.extends-custom-expression.extends-custom-expression

Community Favorite
profile photo of semgrepsemgrep
Author
49,307
Download Count*

Found extension of custom expression: $CLASS. Extending expressions in this way could inadvertently lead to a SQL injection vulnerability, which can result in attackers exfiltrating sensitive data. Instead, ensure no user input enters this function or that user input is properly sanitized.

Run Locally

Run in CI

Defintion

rules:
  - id: extends-custom-expression
    languages:
      - python
    message: "Found extension of custom expression: $CLASS. Extending expressions in
      this way could inadvertently lead to a SQL injection vulnerability, which
      can result in attackers exfiltrating sensitive data. Instead, ensure no
      user input enters this function or that user input is properly sanitized."
    metadata:
      cwe:
        - "CWE-89: Improper Neutralization of Special Elements used in an SQL
          Command ('SQL Injection')"
      owasp:
        - A01:2017 - Injection
        - A03:2021 - Injection
      references:
        - https://docs.djangoproject.com/en/3.0/ref/models/expressions/#avoiding-sql-injection
        - https://semgrep.dev/blog/2020/preventing-sql-injection-a-django-authors-perspective/
      category: security
      technology:
        - django
      cwe2022-top25: true
      cwe2021-top25: true
      subcategory:
        - audit
      likelihood: LOW
      impact: HIGH
      confidence: LOW
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
      vulnerability_class:
        - SQL Injection
    severity: WARNING
    pattern-either:
      - pattern: |
          class $CLASS(..., django.db.models.Func, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.Func, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.Expression, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.Expression, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.Value, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.Value, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.DurationValue, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.DurationValue, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.RawSQL, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.RawSQL, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.Star, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.Star, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.Random, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.Random, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.Col, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.Col, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.Ref, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.Ref, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.ExpressionList, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.ExpressionList, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.ExpressionWrapper, ...):
              ...
      - pattern: >
          class $CLASS(..., django.db.models.expressions.ExpressionWrapper,
          ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.When, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.When, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.Case, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.Case, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.Subquery, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.Subquery, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.Exists, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.Exists, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.Window, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.Window, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.WindowFrame, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.WindowFrame, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.RowRange, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.RowRange, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.ValueRange, ...):
              ...
      - pattern: |
          class $CLASS(..., django.db.models.expressions.ValueRange, ...):
              ...

Examples

extends-custom-expression.py

from django.db.models import (
    CharField, Expression, Field, FloatField, Lookup, TextField, Value,
)
from django.db.models.expressions import CombinedExpression, Func, Subquery
from django.db.models.functions import Cast, Coalesce

# ok: extends-custom-expression
class Star(CharField):
    pass

# ok: extends-custom-expression
class MoreStar(Star):
    pass

# ruleid: extends-custom-expression
class Position(Func):
    function = 'POSITION'
    template = "%(function)s('%(substring)s' in %(expressions)s)"

    # todoruleid: extends-custom-expression
    def __init__(self, expression, substring):
        # substring=substring is a SQL injection vulnerability!
        super().__init__(expression, substring=substring)

# ruleid: extends-custom-expression
class Aggregate(Func):
    template = '%(function)s(%(distinct)s%(expressions)s)'
    contains_aggregate = True
    name = None
    filter_template = '%s FILTER (WHERE %%(filter)s)'
    window_compatible = True
    allow_distinct = False

    def __init__(self, *expressions, distinct=False, filter=None, **extra):
        if distinct and not self.allow_distinct:
            raise TypeError("%s does not allow distinct." % self.__class__.__name__)
        self.distinct = distinct
        self.filter = filter
        super().__init__(*expressions, **extra)

# ruleid: extends-custom-expression
class CastToInteger(Func):
    """
    A helper class for casting values to signed integer in database.
    """
    function = 'CAST'
    template = '%(function)s(%(expressions)s as %(integer_type)s)'

    def __init__(self, *expressions, **extra):
        super().__init__(*expressions, **extra)
        self.extra['integer_type'] = 'INTEGER'

    def as_mysql(self, compiler, connection):
        self.extra['integer_type'] = 'SIGNED'
        return super().as_sql(compiler, connection)

# ruleid: extends-custom-expression
class DateFormat(Func):
    function = 'DATE_FORMAT'
    template = '%(function)s(%(expressions)s, "%(format)s")'

    def __init__(self, *expressions, **extra):
        strf = extra.pop('format', None)
        extra['format'] = strf.replace("%", "%%")
        extra['output_field'] = CharField()
        super(DateFormat, self).__init__(*expressions, **extra)

# ruleid: extends-custom-expression
class StrFtime(Func):
    function = 'strftime'
    template = '%(function)s("%(format)s", %(expressions)s)'

    def __init__(self, *expressions, **extra):
        strf = extra.pop('format', None)
        extra['format'] = strf.replace("%", "%%")
        extra['output_field'] = CharField()
        super(StrFtime, self).__init__(*expressions, **extra)

# ruleid: extends-custom-expression
class RandomUUID(Func):
    template = 'GEN_RANDOM_UUID()'
    output_field = UUIDField()

# ruleid: extends-custom-expression
class SearchHeadline(Func):
    function = 'ts_headline'
    template = '%(function)s(%(expressions)s%(options)s)'
    output_field = TextField()

    def __init__(
        self, expression, query, *, config=None, start_sel=None, stop_sel=None,
        max_words=None, min_words=None, short_word=None, highlight_all=None,
        max_fragments=None, fragment_delimiter=None,
    ):
        if not hasattr(query, 'resolve_expression'):
            query = SearchQuery(query)
        options = {
            'StartSel': start_sel,
            'StopSel': stop_sel,
            'MaxWords': max_words,
            'MinWords': min_words,
            'ShortWord': short_word,
            'HighlightAll': highlight_all,
            'MaxFragments': max_fragments,
            'FragmentDelimiter': fragment_delimiter,
        }
        self.options = {
            option: value
            for option, value in options.items() if value is not None
        }
        expressions = (expression, query)
        if config is not None:
            config = SearchConfig.from_parameter(config)
            expressions = (config,) + expressions
        super().__init__(*expressions)

# ruleid: extends-custom-expression
class _Str(Func):

    """Casts a value to the database's text type."""

    function = "CAST"
    template = "%(function)s(%(expressions)s as %(db_type)s)"

    def __init__(self, expression):
        super().__init__(expression, output_field=models.TextField())

    def as_sql(self, compiler, connection):
        self.extra["db_type"] = self.output_field.db_type(connection)
        return super().as_sql(compiler, connection)

# ruleid: extends-custom-expression
class SQCount(Subquery):
    template = "(SELECT count(*) FROM (%(subquery)s) _count)"
    output_field = IntegerField()