python.django.security.audit.query-set-extra.avoid-query-set-extra

Community Favorite
profile photo of semgrepsemgrep
Author
82,283
Download Count*

QuerySet.extra' does not provide safeguards against SQL injection and requires very careful use. SQL injection can lead to critical data being stolen by attackers. Instead of using '.extra', use the Django ORM and parameterized queries such as People.objects.get(name='Bob').

Run Locally

Run in CI

Defintion

rules:
  - id: avoid-query-set-extra
    message: QuerySet.extra' does not provide safeguards against SQL injection and
      requires very careful use. SQL injection can lead to critical data being
      stolen by attackers. Instead of using '.extra', use the Django ORM and
      parameterized queries such as `People.objects.get(name='Bob')`.
    metadata:
      source-rule-url: https://bandit.readthedocs.io/en/latest/plugins/b610_django_extra_used.html
      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/querysets/#django.db.models.query.QuerySet.extra
        - 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
    languages:
      - python
    severity: WARNING
    patterns:
      - pattern: $MODEL.extra(...)
      - pattern-not-inside: '$MODEL.extra(select = {$KEY: "..."})'

Examples

query-set-extra.py

# ruleid:avoid-query-set-extra
Entry.objects.get().extra()

# ruleid:avoid-query-set-extra
Entry.objects.filter().extra()

# ruleid:avoid-query-set-extra
Entry.objects.update().extra()

# ruleid:avoid-query-set-extra
Entry.objects.filter().update().extra()

# ruleid:avoid-query-set-extra
Entry.objects.get({}).filter().update().extra()

# ok:avoid-query-set-extra
findings = Finding.objects.filter(verified=True,
                                      severity__in=('Critical', 'High', 'Medium', 'Low', 'Info')).prefetch_related(
        'test__engagement__product',
        'test__engagement__product__prod_type',
        'test__engagement__risk_acceptance',
        'risk_acceptance_set',
        'reporter').extra(
        select={
            'ra_count': 'SELECT COUNT(*) FROM dojo_risk_acceptance INNER JOIN '
                        'dojo_risk_acceptance_accepted_findings ON '
                        '( dojo_risk_acceptance.id = dojo_risk_acceptance_accepted_findings.risk_acceptance_id ) '
                        'WHERE dojo_risk_acceptance_accepted_findings.finding_id = dojo_finding.id',
        },
    )

example = 1
# ruleid:avoid-query-set-extra
active_findings = Finding.objects.filter(verified=True, active=True,
                                      severity__in=('Critical', 'High', 'Medium', 'Low', 'Info')).prefetch_related(
        'test__engagement__product',
        'test__engagement__product__prod_type',
        'test__engagement__risk_acceptance',
        'risk_acceptance_set',
        'reporter').extra(
        select={
            'ra_count': f'SELECT COUNT(*) FROM dojo_risk_acceptance INNER JOIN '
                        f'dojo_risk_acceptance_accepted_findings ON '
                        f'( dojo_risk_acceptance.id = dojo_risk_acceptance_accepted_findings.risk_acceptance_id ) '
                        f'WHERE dojo_risk_acceptance_accepted_findings.finding_id = {example}',
        },
    )