Skip to content

Commit ff0583e

Browse files
security: do not reveal products, product types and findings in metrics and reports to unauthorized users (#3410)
* several issues in metrics and reports * one user too many * EndpointFilter with user from request * integration tests + fix metrics js errors * metrics: cache per user * integration tests + fix metrics js errors Co-authored-by: Valentijn Scholten <[email protected]>
1 parent 43142b2 commit ff0583e

File tree

12 files changed

+243
-30
lines changed

12 files changed

+243
-30
lines changed

docker-compose.override.integration_tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ services:
99
depends_on:
1010
- nginx
1111
- uwsgi
12-
entrypoint: ['/wait-for-it.sh', 'mysql:3306', '-t', '30', '--', '/entrypoint-integration-tests.sh']
12+
entrypoint: ['/wait-for-it.sh', 'mysql:3306', '-t', '30', '--', '/app/docker/entrypoint-integration-tests.sh']
13+
volumes:
14+
- '.:/app:z'
1315
environment:
1416
DD_BASE_URL: 'http://nginx:8080/'
1517
DD_ADMIN_USER: ${DD_ADMIN_USER:-admin}

dojo/filters.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
from dojo.models import Dojo_User, Product_Type, Finding, Product, Test_Type, \
1818
Endpoint, Development_Environment, Finding_Template, Report, Note_Type, \
1919
Engagement_Survey, Question, TextQuestion, ChoiceQuestion, Endpoint_Status, Engagement, \
20-
ENGAGEMENT_STATUS_CHOICES
20+
ENGAGEMENT_STATUS_CHOICES, Test
2121
from dojo.utils import get_system_setting
2222
from django.contrib.contenttypes.models import ContentType
23+
from crum import get_current_user
2324

2425
logger = logging.getLogger(__name__)
2526

@@ -1072,6 +1073,18 @@ def __init__(self, *args, **kwargs):
10721073
self.form.fields['severity'].choices = self.queryset.order_by(
10731074
'numerical_severity'
10741075
).values_list('severity', 'severity').distinct()
1076+
if get_current_user() is not None and not get_current_user().is_staff:
1077+
self.form.fields[
1078+
'test__engagement__product__prod_type'].queryset = Product_Type.objects.filter(
1079+
authorized_users__in=[get_current_user()])
1080+
self.form.fields[
1081+
'test'].queryset = Test.objects.filter(
1082+
Q(engagement__product__authorized_users__in=[get_current_user()]) |
1083+
Q(engagement__product__prod_type__authorized_users__in=[get_current_user()]))
1084+
self.form.fields[
1085+
'duplicate_finding'].queryset = Finding.objects.filter(
1086+
Q(test__engagement__product__authorized_users__in=[get_current_user()]) |
1087+
Q(test__engagement__product__prod_type__authorized_users__in=[get_current_user()]))
10751088

10761089
class Meta:
10771090
model = Finding
@@ -1117,6 +1130,18 @@ def __init__(self, *args, **kwargs):
11171130
self.form.fields['finding__severity'].choices = self.queryset.order_by(
11181131
'finding__numerical_severity'
11191132
).values_list('finding__severity', 'finding__severity').distinct()
1133+
if get_current_user() is not None and not get_current_user().is_staff:
1134+
self.form.fields[
1135+
'finding__test__engagement__product__prod_type'].queryset = Product_Type.objects.filter(
1136+
authorized_users__in=[get_current_user()])
1137+
self.form.fields[
1138+
'endpoint'].queryset = Endpoint.objects.filter(
1139+
Q(product__authorized_users__in=[get_current_user()]) |
1140+
Q(product__prod_type__authorized_users__in=[get_current_user()]))
1141+
self.form.fields[
1142+
'finding'].queryset = Finding.objects.filter(
1143+
Q(test__engagement__product__authorized_users__in=[get_current_user()]) |
1144+
Q(test__engagement__product__prod_type__authorized_users__in=[get_current_user()]))
11201145

11211146
class Meta:
11221147
model = Endpoint_Status
@@ -1227,8 +1252,10 @@ def __init__(self, *args, **kwargs):
12271252
self.user = kwargs.pop('user')
12281253
super(EndpointFilter, self).__init__(*args, **kwargs)
12291254
if self.user and not self.user.is_staff:
1230-
self.form.fields['product'].queryset = Product.objects.filter(
1231-
authorized_users__in=[self.user]).distinct().order_by('name')
1255+
self.form.fields[
1256+
'product'].queryset = Product.objects.filter(
1257+
Q(authorized_users__in=[self.user]) |
1258+
Q(prod_type__authorized_users__in=[self.user])).distinct().order_by('name')
12321259

12331260
class Meta:
12341261
model = Endpoint
@@ -1286,28 +1313,30 @@ class ReportAuthedFindingFilter(DojoFilter):
12861313
out_of_scope = ReportBooleanFilter()
12871314

12881315
def __init__(self, *args, **kwargs):
1289-
self.user = None
1290-
if 'user' in kwargs:
1291-
self.user = kwargs.pop('user')
12921316
super(ReportAuthedFindingFilter, self).__init__(*args, **kwargs)
1293-
if not self.user.is_staff:
1317+
if get_current_user() and not get_current_user().is_staff:
12941318
self.form.fields[
12951319
'test__engagement__product'].queryset = Product.objects.filter(
1296-
authorized_users__in=[self.user])
1320+
Q(authorized_users__in=[get_current_user()]) |
1321+
Q(prod_type__authorized_users__in=[get_current_user()]))
12971322
self.form.fields[
12981323
'test__engagement__product__prod_type'].queryset = Product_Type.objects.filter(
1299-
authorized_users__in=[self.user])
1324+
authorized_users__in=[get_current_user()])
1325+
self.form.fields[
1326+
'duplicate_finding'].queryset = Finding.objects.filter(
1327+
Q(test__engagement__product__authorized_users__in=[get_current_user()]) |
1328+
Q(test__engagement__product__prod_type__authorized_users__in=[get_current_user()]))
13001329

13011330
@property
13021331
def qs(self):
13031332
parent = super(ReportAuthedFindingFilter, self).qs
1304-
if self.user.is_staff:
1305-
return parent
1306-
else:
1333+
if get_current_user() and not get_current_user().is_staff:
13071334
return parent.filter(
1308-
Q(test__engagement__product__authorized_users__in=[self.user]) |
1309-
Q(test__engagement__product__prod_type__authorized_users__in=[self.user])
1335+
Q(test__engagement__product__authorized_users__in=[get_current_user()]) |
1336+
Q(test__engagement__product__prod_type__authorized_users__in=[get_current_user()])
13101337
)
1338+
else:
1339+
return parent
13111340

13121341
class Meta:
13131342
model = Finding

dojo/fixtures/product_type.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
[
22
{
33
"fields": {
4-
"name": "Research and Development"
4+
"name": "Research and Development",
5+
"critical_product": true
56
},
67
"model": "dojo.product_type",
78
"pk": 1

dojo/forms.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from dojo.user.helper import user_is_authorized
3333
from django.urls import reverse
3434
import logging
35+
from crum import get_current_user
3536

3637
logger = logging.getLogger(__name__)
3738

@@ -1631,6 +1632,12 @@ class ProductTypeCountsForm(forms.Form):
16311632
error_messages={
16321633
'required': '*'})
16331634

1635+
def __init__(self, *args, **kwargs):
1636+
super(ProductTypeCountsForm, self).__init__(*args, **kwargs)
1637+
if get_current_user() is not None and not get_current_user().is_staff:
1638+
self.fields['product_type'].queryset = Product_Type.objects.filter(
1639+
authorized_users__in=[get_current_user()])
1640+
16341641

16351642
class APIKeyForm(forms.ModelForm):
16361643
id = forms.IntegerField(required=True,

dojo/metrics/views.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
from dojo.utils import get_page_items, add_breadcrumb, findings_this_period, opened_in_period, count_findings, \
2828
get_period_counts, get_system_setting, get_punchcard_data, queryset_check
2929
from functools import reduce
30+
from dojo.user.helper import objects_authorized
31+
from django.views.decorators.vary import vary_on_cookie
3032

3133
logger = logging.getLogger(__name__)
3234

@@ -41,6 +43,7 @@ def critical_product_metrics(request, mtype):
4143
template = 'dojo/metrics.html'
4244
page_name = 'Critical Product Metrics'
4345
critical_products = Product_Type.objects.filter(critical_product=True)
46+
critical_products = objects_authorized(critical_products)
4447
add_breadcrumb(title=page_name, top_level=not len(request.GET), request=request)
4548
return render(request, template, {
4649
'name': page_name,
@@ -125,6 +128,11 @@ def finding_querys(prod_type, request):
125128
'WHERE dojo_risk_acceptance_accepted_findings.finding_id = dojo_finding.id',
126129
},
127130
)
131+
if not request.user.is_staff:
132+
findings_query = findings_query.filter(
133+
Q(test__engagement__product__authorized_users__in=[request.user]) |
134+
Q(test__engagement__product__prod_type__authorized_users__in=[request.user]))
135+
128136
active_findings_query = Finding.objects.filter(verified=True, active=True,
129137
severity__in=('Critical', 'High', 'Medium', 'Low', 'Info')).prefetch_related(
130138
'test__engagement__product',
@@ -139,6 +147,10 @@ def finding_querys(prod_type, request):
139147
'WHERE dojo_risk_acceptance_accepted_findings.finding_id = dojo_finding.id',
140148
},
141149
)
150+
if not request.user.is_staff:
151+
active_findings_query = active_findings_query.filter(
152+
Q(test__engagement__product__authorized_users__in=[request.user]) |
153+
Q(test__engagement__product__prod_type__authorized_users__in=[request.user]))
142154

143155
findings = MetricsFindingFilter(request.GET, queryset=findings_query)
144156
active_findings = MetricsFindingFilter(request.GET, queryset=active_findings_query)
@@ -180,6 +192,10 @@ def finding_querys(prod_type, request):
180192
accepted_findings_counts = Finding.objects.filter(risk_acceptance__created__date__range=[start_date, end_date],
181193
test__engagement__product__prod_type__in=prod_type). \
182194
prefetch_related('test__engagement__product')
195+
if not request.user.is_staff:
196+
accepted_findings_counts = accepted_findings_counts.filter(
197+
Q(test__engagement__product__authorized_users__in=[request.user]) |
198+
Q(test__engagement__product__prod_type__authorized_users__in=[request.user]))
183199
accepted_findings_counts = severity_count(accepted_findings_counts, 'aggregate', 'severity')
184200
else:
185201
findings_closed = Finding.objects.filter(mitigated__date__range=[start_date, end_date]).prefetch_related(
@@ -188,8 +204,20 @@ def finding_querys(prod_type, request):
188204
prefetch_related('test__engagement__product')
189205
accepted_findings_counts = Finding.objects.filter(risk_acceptance__created__date__range=[start_date, end_date]). \
190206
prefetch_related('test__engagement__product')
207+
if not request.user.is_staff:
208+
accepted_findings_counts = accepted_findings_counts.filter(
209+
Q(test__engagement__product__authorized_users__in=[request.user]) |
210+
Q(test__engagement__product__prod_type__authorized_users__in=[request.user]))
191211
accepted_findings_counts = severity_count(accepted_findings_counts, 'aggregate', 'severity')
192212

213+
if not request.user.is_staff:
214+
findings_closed = findings_closed.filter(
215+
Q(test__engagement__product__authorized_users__in=[request.user]) |
216+
Q(test__engagement__product__prod_type__authorized_users__in=[request.user]))
217+
accepted_findings = accepted_findings.filter(
218+
Q(test__engagement__product__authorized_users__in=[request.user]) |
219+
Q(test__engagement__product__prod_type__authorized_users__in=[request.user]))
220+
193221
r = relativedelta(end_date, start_date)
194222
months_between = (r.years * 12) + r.months
195223
# include current month
@@ -212,6 +240,10 @@ def finding_querys(prod_type, request):
212240
engagement__test__finding__severity__in=(
213241
'Critical', 'High', 'Medium', 'Low'),
214242
prod_type__in=prod_type)
243+
if not request.user.is_staff:
244+
top_ten = top_ten.filter(
245+
Q(authorized_users__in=[request.user]) |
246+
Q(prod_type__authorized_users__in=[request.user]))
215247
top_ten = severity_count(top_ten, 'annotate', 'engagement__test__finding__severity').order_by('-critical', '-high', '-medium', '-low')[:10]
216248

217249
filters['all'] = findings
@@ -238,6 +270,10 @@ def endpoint_querys(prod_type, request):
238270
'finding__test__engagement__risk_acceptance',
239271
'finding__risk_acceptance_set',
240272
'finding__reporter')
273+
if not request.user.is_staff:
274+
endpoints_query = endpoints_query.filter(
275+
Q(endpoint__product__authorized_users__in=[request.user]) |
276+
Q(endpoint__product__prod_type__authorized_users__in=[request.user]))
241277

242278
active_endpoints_query = Endpoint_Status.objects.filter(mitigated=False,
243279
finding__severity__in=('Critical', 'High', 'Medium', 'Low', 'Info')).prefetch_related(
@@ -246,6 +282,10 @@ def endpoint_querys(prod_type, request):
246282
'finding__test__engagement__risk_acceptance',
247283
'finding__risk_acceptance_set',
248284
'finding__reporter')
285+
if not request.user.is_staff:
286+
active_endpoints_query = active_endpoints_query.filter(
287+
Q(endpoint__product__authorized_users__in=[request.user]) |
288+
Q(endpoint__product__prod_type__authorized_users__in=[request.user]))
249289

250290
endpoints = MetricsEndpointFilter(request.GET, queryset=endpoints_query)
251291
active_endpoints = MetricsEndpointFilter(request.GET, queryset=active_endpoints_query)
@@ -287,6 +327,10 @@ def endpoint_querys(prod_type, request):
287327
accepted_endpoints_counts = Endpoint_Status.objects.filter(date__range=[start_date, end_date], risk_accepted=True,
288328
finding__test__engagement__product__prod_type__in=prod_type). \
289329
prefetch_related('finding__test__engagement__product')
330+
if not request.user.is_staff:
331+
accepted_endpoints_counts = accepted_endpoints_counts.filter(
332+
Q(endpoint__product__authorized_users__in=[request.user]) |
333+
Q(endpoint__product__prod_type__authorized_users__in=[request.user]))
290334
accepted_endpoints_counts = severity_count(accepted_endpoints_counts, 'aggregate', 'finding__severity')
291335
else:
292336
endpoints_closed = Endpoint_Status.objects.filter(mitigated__date__range=[start_date, end_date]).prefetch_related(
@@ -295,8 +339,20 @@ def endpoint_querys(prod_type, request):
295339
prefetch_related('finding__test__engagement__product')
296340
accepted_endpoints_counts = Endpoint_Status.objects.filter(date__range=[start_date, end_date], risk_accepted=True). \
297341
prefetch_related('finding__test__engagement__product')
342+
if not request.user.is_staff:
343+
accepted_endpoints_counts = accepted_endpoints_counts.filter(
344+
Q(endpoint__product__authorized_users__in=[request.user]) |
345+
Q(endpoint__product__prod_type__authorized_users__in=[request.user]))
298346
accepted_endpoints_counts = severity_count(accepted_endpoints_counts, 'aggregate', 'finding__severity')
299347

348+
if not request.user.is_staff:
349+
endpoints_closed = endpoints_closed.filter(
350+
Q(endpoint__product__authorized_users__in=[request.user]) |
351+
Q(endpoint__product__prod_type__authorized_users__in=[request.user]))
352+
accepted_endpoints = accepted_endpoints.filter(
353+
Q(endpoint__product__authorized_users__in=[request.user]) |
354+
Q(endpoint__product__prod_type__authorized_users__in=[request.user]))
355+
300356
r = relativedelta(end_date, start_date)
301357
months_between = (r.years * 12) + r.months
302358
# include current month
@@ -317,6 +373,10 @@ def endpoint_querys(prod_type, request):
317373
engagement__test__finding__severity__in=(
318374
'Critical', 'High', 'Medium', 'Low'),
319375
prod_type__in=prod_type)
376+
if not request.user.is_staff:
377+
top_ten = top_ten.filter(
378+
Q(authorized_users__in=[request.user]) |
379+
Q(prod_type__authorized_users__in=[request.user]))
320380
top_ten = severity_count(top_ten, 'annotate', 'engagement__test__finding__severity').order_by('-critical', '-high', '-medium', '-low')[:10]
321381

322382
filters['all'] = endpoints
@@ -334,6 +394,7 @@ def endpoint_querys(prod_type, request):
334394

335395

336396
@cache_page(60 * 5) # cache for 5 minutes
397+
@vary_on_cookie
337398
def metrics(request, mtype):
338399
template = 'dojo/metrics.html'
339400
show_pt_filter = True
@@ -362,6 +423,7 @@ def metrics(request, mtype):
362423
prod_type = Product_Type.objects.filter(id__in=request.GET.getlist('test__engagement__product__prod_type', []))
363424
else:
364425
prod_type = Product_Type.objects.all()
426+
prod_type = objects_authorized(prod_type)
365427

366428
filters = dict()
367429
if view == 'Finding':
@@ -470,6 +532,7 @@ def metrics(request, mtype):
470532

471533

472534
@cache_page(60 * 5) # cache for 5 minutes
535+
@vary_on_cookie
473536
def simple_metrics(request):
474537
now = timezone.now()
475538

@@ -485,7 +548,9 @@ def simple_metrics(request):
485548

486549
# for each product type find each product with open findings and
487550
# count the S0, S1, S2 and S3
488-
for pt in Product_Type.objects.order_by('name'):
551+
product_types = Product_Type.objects.order_by('name')
552+
product_types = objects_authorized(product_types)
553+
for pt in product_types:
489554
total_critical = []
490555
total_high = []
491556
total_medium = []
@@ -503,8 +568,9 @@ def simple_metrics(request):
503568
date__month=now.month,
504569
date__year=now.year,
505570
).distinct()
571+
total = objects_authorized(total.all())
506572

507-
for f in total.all():
573+
for f in total:
508574
if f.severity == "Critical":
509575
total_critical.append(f)
510576
elif f.severity == 'High':
@@ -522,7 +588,7 @@ def simple_metrics(request):
522588
if f.date.year == now.year and f.date.month == now.month:
523589
total_opened.append(f)
524590

525-
findings_broken_out['Total'] = total.count()
591+
findings_broken_out['Total'] = len(total)
526592
findings_broken_out['S0'] = len(total_critical)
527593
findings_broken_out['S1'] = len(total_high)
528594
findings_broken_out['S2'] = len(total_medium)
@@ -546,6 +612,7 @@ def simple_metrics(request):
546612

547613

548614
# @cache_page(60 * 5) # cache for 5 minutes
615+
# @vary_on_cookie
549616
def product_type_counts(request):
550617
form = ProductTypeCountsForm()
551618
opened_in_period_list = []
@@ -645,6 +712,10 @@ def product_type_counts(request):
645712
'test__engagement__risk_acceptance',
646713
'reporter').order_by(
647714
'numerical_severity')
715+
if not request.user.is_staff:
716+
all_current_in_pt = all_current_in_pt.filter(
717+
Q(test__engagement__product__authorized_users__in=[request.user]) |
718+
Q(test__engagement__product__prod_type__authorized_users__in=[request.user]))
648719

649720
top_ten = Product.objects.filter(engagement__test__finding__date__lte=end_date,
650721
engagement__test__finding__verified=True,
@@ -655,6 +726,10 @@ def product_type_counts(request):
655726
engagement__test__finding__severity__in=(
656727
'Critical', 'High', 'Medium', 'Low'),
657728
prod_type=pt)
729+
if not request.user.is_staff:
730+
top_ten = top_ten.filter(
731+
Q(authorized_users__in=[request.user]) |
732+
Q(prod_type__authorized_users__in=[request.user]))
658733
top_ten = severity_count(top_ten, 'annotate', 'engagement__test__finding__severity').order_by('-critical', '-high', '-medium', '-low')[:10]
659734

660735
cip = {'S0': 0,
@@ -733,6 +808,7 @@ def engineer_metrics(request):
733808
# noinspection DjangoOrm
734809
@user_passes_test(lambda u: u.is_staff)
735810
@cache_page(60 * 5) # cache for 5 minutes
811+
@vary_on_cookie
736812
def view_engineer(request, eid):
737813
user = get_object_or_404(Dojo_User, pk=eid)
738814
if not (request.user.is_superuser or

0 commit comments

Comments
 (0)