summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2022-04-01 13:48:47 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-04-11 08:59:58 +0200
commit6723a26e59b0b5429a0c5873941e01a2e1bdbb81 (patch)
tree59bbe514736c482903de4d92046e9f58594680d3 /django
parent93cae5cb2f9a4ef1514cf1a41f714fef08005200 (diff)
Fixed CVE-2022-28347 -- Protected QuerySet.explain(**options) against SQL injection on PostgreSQL.
Diffstat (limited to 'django')
-rw-r--r--django/db/backends/postgresql/features.py1
-rw-r--r--django/db/backends/postgresql/operations.py31
-rw-r--r--django/db/models/sql/query.py10
3 files changed, 33 insertions, 9 deletions
diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py
index 5e6752b97a..8aae4caf34 100644
--- a/django/db/backends/postgresql/features.py
+++ b/django/db/backends/postgresql/features.py
@@ -54,7 +54,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
only_supports_unbounded_with_preceding_and_following = True
supports_aggregate_filter_clause = True
supported_explain_formats = {"JSON", "TEXT", "XML", "YAML"}
- validates_explain_options = False # A query will error on invalid options.
supports_deferrable_unique_constraints = True
has_json_operators = True
json_key_contains_list_matching_requires_list = True
diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py
index 946baea212..ab451ac63f 100644
--- a/django/db/backends/postgresql/operations.py
+++ b/django/db/backends/postgresql/operations.py
@@ -9,6 +9,18 @@ from django.db.models.constants import OnConflict
class DatabaseOperations(BaseDatabaseOperations):
cast_char_field_without_max_length = "varchar"
explain_prefix = "EXPLAIN"
+ explain_options = frozenset(
+ [
+ "ANALYZE",
+ "BUFFERS",
+ "COSTS",
+ "SETTINGS",
+ "SUMMARY",
+ "TIMING",
+ "VERBOSE",
+ "WAL",
+ ]
+ )
cast_data_types = {
"AutoField": "integer",
"BigAutoField": "bigint",
@@ -298,17 +310,20 @@ class DatabaseOperations(BaseDatabaseOperations):
return super().subtract_temporals(internal_type, lhs, rhs)
def explain_query_prefix(self, format=None, **options):
- prefix = super().explain_query_prefix(format)
extra = {}
+ # Normalize options.
+ if options:
+ options = {
+ name.upper(): "true" if value else "false"
+ for name, value in options.items()
+ }
+ for valid_option in self.explain_options:
+ value = options.pop(valid_option, None)
+ if value is not None:
+ extra[valid_option.upper()] = value
+ prefix = super().explain_query_prefix(format, **options)
if format:
extra["FORMAT"] = format
- if options:
- extra.update(
- {
- name.upper(): "true" if value else "false"
- for name, value in options.items()
- }
- )
if extra:
prefix += " (%s)" % ", ".join("%s %s" % i for i in extra.items())
return prefix
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index 894aa7db4a..a55eb84a17 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -49,6 +49,10 @@ __all__ = ["Query", "RawQuery"]
# SQL comments are forbidden in column aliases.
FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|--|/\*|\*/")
+# Inspired from
+# https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
+EXPLAIN_OPTIONS_PATTERN = _lazy_re_compile(r"[\w\-]+")
+
def get_field_names_from_opts(opts):
if opts is None:
@@ -589,6 +593,12 @@ class Query(BaseExpression):
def explain(self, using, format=None, **options):
q = self.clone()
+ for option_name in options:
+ if (
+ not EXPLAIN_OPTIONS_PATTERN.fullmatch(option_name)
+ or "--" in option_name
+ ):
+ raise ValueError(f"Invalid option name: {option_name!r}.")
q.explain_info = ExplainInfo(format, options)
compiler = q.get_compiler(using=using)
return "\n".join(compiler.explain_query())