Skip to content

Commit 38e1bd7

Browse files
Merge pull request from GHSA-pghf-347x-c2gj
* Fix CVE-2021-30459 by creating signature from all data fields. Create a signature based on all fields in the form and attach to validate that the data being sent back is what the server generated initially. Change the hashing algorithm to SHA256 Force the values to a string for signing. Remove hashing mechanism from forms. Support sha1 algorithm for django < 3.1 * Bump version to 3.2.1
1 parent 8b280e1 commit 38e1bd7

16 files changed

+212
-138
lines changed

README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Here's a screenshot of the toolbar in action:
3434
In addition to the built-in panels, a number of third-party panels are
3535
contributed by the community.
3636

37-
The current stable version of the Debug Toolbar is 3.2. It works on
37+
The current stable version of the Debug Toolbar is 3.2.1. It works on
3838
Django ≥ 2.2.
3939

4040
Documentation, including installation and configuration instructions, is

debug_toolbar/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
# Do not use pkg_resources to find the version but set it here directly!
55
# see issue #1446
6-
VERSION = "3.2"
6+
VERSION = "3.2.1"
77

88
# Code that discovers files or modules in INSTALLED_APPS imports this module.
99

debug_toolbar/decorators.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import functools
22

3-
from django.http import Http404
3+
from django.http import Http404, HttpResponseBadRequest
44

55

66
def require_show_toolbar(view):
@@ -15,3 +15,21 @@ def inner(request, *args, **kwargs):
1515
return view(request, *args, **kwargs)
1616

1717
return inner
18+
19+
20+
def signed_data_view(view):
21+
"""Decorator that handles unpacking a signed data form"""
22+
23+
@functools.wraps(view)
24+
def inner(request, *args, **kwargs):
25+
from debug_toolbar.forms import SignedDataForm
26+
27+
data = request.GET if request.method == "GET" else request.POST
28+
signed_form = SignedDataForm(data)
29+
if signed_form.is_valid():
30+
return view(
31+
request, *args, verified_data=signed_form.verified_data(), **kwargs
32+
)
33+
return HttpResponseBadRequest("Invalid signature")
34+
35+
return inner

debug_toolbar/forms.py

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import json
2+
3+
from django import forms
4+
from django.core import signing
5+
from django.core.exceptions import ValidationError
6+
from django.utils.encoding import force_str
7+
8+
9+
class SignedDataForm(forms.Form):
10+
"""Helper form that wraps a form to validate its contents on post.
11+
12+
class PanelForm(forms.Form):
13+
# fields
14+
15+
On render:
16+
form = SignedDataForm(initial=PanelForm(initial=data).initial)
17+
18+
On POST:
19+
signed_form = SignedDataForm(request.POST)
20+
if signed_form.is_valid():
21+
panel_form = PanelForm(signed_form.verified_data)
22+
if panel_form.is_valid():
23+
# Success
24+
Or wrap the FBV with ``debug_toolbar.decorators.signed_data_view``
25+
"""
26+
27+
salt = "django_debug_toolbar"
28+
signed = forms.CharField(required=True, widget=forms.HiddenInput)
29+
30+
def __init__(self, *args, **kwargs):
31+
initial = kwargs.pop("initial", None)
32+
if initial:
33+
initial = {"signed": self.sign(initial)}
34+
super().__init__(*args, initial=initial, **kwargs)
35+
36+
def clean_signed(self):
37+
try:
38+
verified = json.loads(
39+
signing.Signer(salt=self.salt).unsign(self.cleaned_data["signed"])
40+
)
41+
return verified
42+
except signing.BadSignature:
43+
raise ValidationError("Bad signature")
44+
45+
def verified_data(self):
46+
return self.is_valid() and self.cleaned_data["signed"]
47+
48+
@classmethod
49+
def sign(cls, data):
50+
items = sorted(data.items(), key=lambda item: item[0])
51+
return signing.Signer(salt=cls.salt).sign(
52+
json.dumps({key: force_str(value) for key, value in items})
53+
)

debug_toolbar/panels/history/forms.py

-30
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
import hashlib
2-
import hmac
3-
41
from django import forms
5-
from django.conf import settings
6-
from django.core.exceptions import ValidationError
7-
from django.utils.crypto import constant_time_compare
8-
from django.utils.encoding import force_bytes
92

103

114
class HistoryStoreForm(forms.Form):
@@ -16,26 +9,3 @@ class HistoryStoreForm(forms.Form):
169
"""
1710

1811
store_id = forms.CharField(widget=forms.HiddenInput())
19-
hash = forms.CharField(widget=forms.HiddenInput())
20-
21-
def __init__(self, *args, **kwargs):
22-
initial = kwargs.get("initial", None)
23-
24-
if initial is not None:
25-
initial["hash"] = self.make_hash(initial)
26-
27-
super().__init__(*args, **kwargs)
28-
29-
@staticmethod
30-
def make_hash(data):
31-
m = hmac.new(key=force_bytes(settings.SECRET_KEY), digestmod=hashlib.sha1)
32-
m.update(force_bytes(data["store_id"]))
33-
return m.hexdigest()
34-
35-
def clean_hash(self):
36-
hash = self.cleaned_data["hash"]
37-
38-
if not constant_time_compare(hash, self.make_hash(self.data)):
39-
raise ValidationError("Tamper alert")
40-
41-
return hash

debug_toolbar/panels/history/panel.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.utils import timezone
99
from django.utils.translation import gettext_lazy as _
1010

11+
from debug_toolbar.forms import SignedDataForm
1112
from debug_toolbar.panels import Panel
1213
from debug_toolbar.panels.history import views
1314
from debug_toolbar.panels.history.forms import HistoryStoreForm
@@ -76,16 +77,20 @@ def content(self):
7677
for id, toolbar in reversed(self.toolbar._store.items()):
7778
stores[id] = {
7879
"toolbar": toolbar,
79-
"form": HistoryStoreForm(initial={"store_id": id}),
80+
"form": SignedDataForm(
81+
initial=HistoryStoreForm(initial={"store_id": id}).initial
82+
),
8083
}
8184

8285
return render_to_string(
8386
self.template,
8487
{
8588
"current_store_id": self.toolbar.store_id,
8689
"stores": stores,
87-
"refresh_form": HistoryStoreForm(
88-
initial={"store_id": self.toolbar.store_id}
90+
"refresh_form": SignedDataForm(
91+
initial=HistoryStoreForm(
92+
initial={"store_id": self.toolbar.store_id}
93+
).initial
8994
),
9095
},
9196
)

debug_toolbar/panels/history/views.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from django.http import HttpResponseBadRequest, JsonResponse
22
from django.template.loader import render_to_string
33

4-
from debug_toolbar.decorators import require_show_toolbar
4+
from debug_toolbar.decorators import require_show_toolbar, signed_data_view
55
from debug_toolbar.panels.history.forms import HistoryStoreForm
66
from debug_toolbar.toolbar import DebugToolbar
77

88

99
@require_show_toolbar
10-
def history_sidebar(request):
10+
@signed_data_view
11+
def history_sidebar(request, verified_data):
1112
"""Returns the selected debug toolbar history snapshot."""
12-
form = HistoryStoreForm(request.GET)
13+
form = HistoryStoreForm(verified_data)
1314

1415
if form.is_valid():
1516
store_id = form.cleaned_data["store_id"]
@@ -32,9 +33,10 @@ def history_sidebar(request):
3233

3334

3435
@require_show_toolbar
35-
def history_refresh(request):
36+
@signed_data_view
37+
def history_refresh(request, verified_data):
3638
"""Returns the refreshed list of table rows for the History Panel."""
37-
form = HistoryStoreForm(request.GET)
39+
form = HistoryStoreForm(verified_data)
3840

3941
if form.is_valid():
4042
requests = []

debug_toolbar/panels/sql/forms.py

-31
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
import hashlib
2-
import hmac
31
import json
42

53
from django import forms
6-
from django.conf import settings
74
from django.core.exceptions import ValidationError
85
from django.db import connections
9-
from django.utils.crypto import constant_time_compare
10-
from django.utils.encoding import force_bytes
116
from django.utils.functional import cached_property
127

138
from debug_toolbar.panels.sql.utils import reformat_sql
@@ -21,25 +16,13 @@ class SQLSelectForm(forms.Form):
2116
raw_sql: The sql statement with placeholders
2217
params: JSON encoded parameter values
2318
duration: time for SQL to execute passed in from toolbar just for redisplay
24-
hash: the hash of (secret + sql + params) for tamper checking
2519
"""
2620

2721
sql = forms.CharField()
2822
raw_sql = forms.CharField()
2923
params = forms.CharField()
3024
alias = forms.CharField(required=False, initial="default")
3125
duration = forms.FloatField()
32-
hash = forms.CharField()
33-
34-
def __init__(self, *args, **kwargs):
35-
initial = kwargs.get("initial")
36-
if initial is not None:
37-
initial["hash"] = self.make_hash(initial)
38-
39-
super().__init__(*args, **kwargs)
40-
41-
for name in self.fields:
42-
self.fields[name].widget = forms.HiddenInput()
4326

4427
def clean_raw_sql(self):
4528
value = self.cleaned_data["raw_sql"]
@@ -65,23 +48,9 @@ def clean_alias(self):
6548

6649
return value
6750

68-
def clean_hash(self):
69-
hash = self.cleaned_data["hash"]
70-
71-
if not constant_time_compare(hash, self.make_hash(self.data)):
72-
raise ValidationError("Tamper alert")
73-
74-
return hash
75-
7651
def reformat_sql(self):
7752
return reformat_sql(self.cleaned_data["sql"], with_toggle=False)
7853

79-
def make_hash(self, data):
80-
m = hmac.new(key=force_bytes(settings.SECRET_KEY), digestmod=hashlib.sha1)
81-
for item in [data["sql"], data["params"]]:
82-
m.update(force_bytes(item))
83-
return m.hexdigest()
84-
8554
@property
8655
def connection(self):
8756
return connections[self.cleaned_data["alias"]]

debug_toolbar/panels/sql/panel.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.urls import path
88
from django.utils.translation import gettext_lazy as _, ngettext_lazy as __
99

10+
from debug_toolbar.forms import SignedDataForm
1011
from debug_toolbar.panels import Panel
1112
from debug_toolbar.panels.sql import views
1213
from debug_toolbar.panels.sql.forms import SQLSelectForm
@@ -211,7 +212,9 @@ def duplicate_key(query):
211212
query["vendor"], query["trans_status"]
212213
)
213214

214-
query["form"] = SQLSelectForm(auto_id=None, initial=copy(query))
215+
query["form"] = SignedDataForm(
216+
auto_id=None, initial=SQLSelectForm(initial=copy(query)).initial
217+
)
215218

216219
if query["sql"]:
217220
query["sql"] = reformat_sql(query["sql"], with_toggle=True)

debug_toolbar/panels/sql/views.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
from django.template.loader import render_to_string
33
from django.views.decorators.csrf import csrf_exempt
44

5-
from debug_toolbar.decorators import require_show_toolbar
5+
from debug_toolbar.decorators import require_show_toolbar, signed_data_view
66
from debug_toolbar.panels.sql.forms import SQLSelectForm
77

88

99
@csrf_exempt
1010
@require_show_toolbar
11-
def sql_select(request):
11+
@signed_data_view
12+
def sql_select(request, verified_data):
1213
"""Returns the output of the SQL SELECT statement"""
13-
form = SQLSelectForm(request.POST or None)
14+
form = SQLSelectForm(verified_data)
1415

1516
if form.is_valid():
1617
sql = form.cleaned_data["raw_sql"]
@@ -34,9 +35,10 @@ def sql_select(request):
3435

3536
@csrf_exempt
3637
@require_show_toolbar
37-
def sql_explain(request):
38+
@signed_data_view
39+
def sql_explain(request, verified_data):
3840
"""Returns the output of the SQL EXPLAIN on the given query"""
39-
form = SQLSelectForm(request.POST or None)
41+
form = SQLSelectForm(verified_data)
4042

4143
if form.is_valid():
4244
sql = form.cleaned_data["raw_sql"]
@@ -69,9 +71,10 @@ def sql_explain(request):
6971

7072
@csrf_exempt
7173
@require_show_toolbar
72-
def sql_profile(request):
74+
@signed_data_view
75+
def sql_profile(request, verified_data):
7376
"""Returns the output of running the SQL and getting the profiling statistics"""
74-
form = SQLSelectForm(request.POST or None)
77+
form = SQLSelectForm(verified_data)
7578

7679
if form.is_valid():
7780
sql = form.cleaned_data["raw_sql"]

docs/changes.rst

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ Change log
44
Next version
55
------------
66

7+
8+
3.2.1 (2021-04-14)
9+
------------------
10+
11+
* Fixed SQL Injection vulnerability, CVE-2021-30459. The toolbar now
12+
calculates a signature on all fields for the SQL select, explain,
13+
and analyze forms.
714
* Changed ``djdt.cookie.set()`` to set ``sameSite=Lax`` by default if
815
callers do not provide a value.
916
* Added ``PRETTIFY_SQL`` configuration option to support controlling

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
copyright = copyright.format(datetime.date.today().year)
2626

2727
# The full version, including alpha/beta/rc tags
28-
release = "3.2"
28+
release = "3.2.1"
2929

3030

3131
# -- General configuration ---------------------------------------------------

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = django-debug-toolbar
3-
version = 3.2
3+
version = 3.2.1
44
description = A configurable set of panels that display various debug information about the current request/response.
55
long_description = file: README.rst
66
author = Rob Hudson

0 commit comments

Comments
 (0)