Skip to content

Commit

Permalink
[fix] Make sure to HTML escape all dashboard labels
Browse files Browse the repository at this point in the history
  • Loading branch information
nemesifier committed Nov 23, 2023
1 parent d3fce38 commit 746b146
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 3 deletions.
12 changes: 11 additions & 1 deletion openwisp_utils/admin_theme/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import copy
import html

from django.core.exceptions import ImproperlyConfigured
from django.db.models import Count
Expand Down Expand Up @@ -145,7 +146,12 @@ def get_dashboard_context(request):
aggregate = query_params.get('aggregate')
org_field = query_params.get('organization_field')
default_org_field = 'organization_id'

labels_i18n = value.get('labels')
# HTML escape labels defined in configuration to prevent breaking the JS
if labels_i18n:
for label_key, label_value in labels_i18n.items():
labels_i18n[label_key] = html.escape(label_value)

try:
model = load_model(app_label, model_name)
Expand Down Expand Up @@ -216,9 +222,13 @@ def get_dashboard_context(request):
if labels_i18n and qs_key in labels_i18n:
# store original label as filter, but only
# if we have more than the empty default label defined
# if len(labels_i18n.keys()) > 1
filters.append(label)
label = labels_i18n[qs_key]
else:
# HTML escape labels coming from values in the DB
# to avoid possible XSS attacks caused by
# malicious DB values set by users
label = html.escape(label)
labels.append(label)
# use predefined colors if available,
# otherwise the JS lib will choose automatically
Expand Down
4 changes: 3 additions & 1 deletion tests/test_project/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ def register_dashboard_charts(self):
'without_operator__sum': '#353c44',
},
'labels': {
'with_operator__sum': _('Projects with operators'),
# the <strong> is for testing purposes to
# verify it's being HTML escaped correctly
'with_operator__sum': _('<strong>Projects with operators</strong>'),
'without_operator__sum': _('Projects without operators'),
},
'filters': {
Expand Down
25 changes: 24 additions & 1 deletion tests/test_project/tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)
from openwisp_utils.admin_theme.dashboard import get_dashboard_context

from ..models import Project, RadiusAccounting
from ..models import Operator, Project, RadiusAccounting
from . import AdminTestMixin, CreateMixin
from .utils import MockRequest, MockUser

Expand Down Expand Up @@ -271,3 +271,26 @@ def test_dashboard_disabled(self):
with self.subTest('Test "Dashboard" is absent from menu items'):
response = self.client.get(reverse('admin:index'))
self.assertNotContains(response, 'Dashboard')

def test_get_dashboard_context_html_escape(self):
# craft malicious DB value which will be shown in labels
project = Project.objects.create(name='<script>alert(1)</script>')
Operator.objects.create(project=project, first_name='xss', last_name='xss')
# prepare mock request and get context
mocked_user = MockUser(is_superuser=True)
mocked_request = MockRequest(user=mocked_user)
context = get_dashboard_context(mocked_request)
# ensure DB value is escaped
self.assertEqual(
context['dashboard_charts'][0]['query_params']['labels'][0],
'&lt;script&gt;alert(1)&lt;/script&gt;',
)
# ensure configured labels are escaped
self.assertEqual(
context['dashboard_charts'][1]['labels']['with_operator__sum'],
'&lt;strong&gt;Projects with operators&lt;/strong&gt;',
)
self.assertEqual(
context['dashboard_charts'][1]['query_params']['labels'][0],
'&lt;strong&gt;Projects with operators&lt;/strong&gt;',
)

0 comments on commit 746b146

Please sign in to comment.