Skip to content

Commit 4674923

Browse files
wwsean08tclancy
authored andcommitted
Prevent sensitive information from being logged (#65)
* Rearrange logging to prevent logging secrets This change logs the body of the request after the view has run in order to allow django to mark sensitive post parameters and cause them to be replaced with astericts in the logs. See https://docs.djangoproject.com/en/2.2/howto/error-reporting/#django.views.decorators.debug.sensitive_post_parameters for more info * use chunked_to_max and cleanup a long line * Fix failing unit tests * simplify the logic and add a unit test to validate that the sensitive parameters are not logged * Make changes based on feedback
1 parent e2e5a0e commit 4674923

File tree

2 files changed

+54
-14
lines changed

2 files changed

+54
-14
lines changed

request_logging/middleware.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# Django < 1.10
1010
from django.core.urlresolvers import resolve, Resolver404
1111
from django.utils.termcolors import colorize
12+
from django.views.debug import SafeExceptionReporterFilter
1213

1314
DEFAULT_LOG_LEVEL = logging.DEBUG
1415
DEFAULT_HTTP_4XX_LOG_LEVEL = logging.ERROR
@@ -95,6 +96,7 @@ def __init__(self, get_response=None):
9596
def __call__(self, request):
9697
self.process_request( request )
9798
response = self.get_response( request )
99+
self.process_body(request)
98100
self.process_response( request, response )
99101
return response
100102

@@ -153,24 +155,33 @@ def _log_request(self, request):
153155
logging_context = self._get_logging_context(request, None)
154156
self.logger.log(logging.INFO, method_path, logging_context)
155157
self._log_request_headers(request, logging_context)
156-
self._log_request_body(request, logging_context)
157158

158159
def _log_request_headers(self, request, logging_context):
159160
headers = {k: v for k, v in request.META.items() if k.startswith('HTTP_')}
160161

161162
if headers:
162163
self.logger.log(self.log_level, headers, logging_context)
163164

164-
def _log_request_body(self, request, logging_context):
165-
if request.body:
166-
content_type = request.META.get('CONTENT_TYPE', '')
167-
is_multipart = content_type.startswith('multipart/form-data')
168-
if is_multipart:
169-
self.boundary = '--' + content_type[30:] # First 30 characters are "multipart/form-data; boundary="
170-
if is_multipart:
171-
self._log_multipart(self._chunked_to_max(request.body), logging_context)
172-
else:
173-
self.logger.log(self.log_level, self._chunked_to_max(request.body), logging_context)
165+
def process_body(self, request):
166+
if not request.body:
167+
return
168+
should_skip_logging = self._should_log_route(request)
169+
if should_skip_logging:
170+
return
171+
logging_context = self._get_logging_context(request, None)
172+
content_type = request.META.get('CONTENT_TYPE', '')
173+
is_multipart = content_type.startswith('multipart/form-data')
174+
to_log = None
175+
if is_multipart:
176+
self.boundary = '--' + content_type[30:] # First 30 characters are "multipart/form-data; boundary="
177+
self._log_multipart(self._chunked_to_max(request.body), logging_context)
178+
elif request.POST:
179+
to_log = str(SafeExceptionReporterFilter().get_post_parameters(request).dict())
180+
else:
181+
to_log = request.body
182+
183+
if to_log:
184+
self.logger.log(self.log_level, self._chunked_to_max(to_log), logging_context)
174185

175186
def process_response(self, request, response):
176187
resp_log = "{} {} - {}".format(request.method, request.get_full_path(), response.status_code)

tests.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import sys
99
from django.conf import settings
1010
from django.test import RequestFactory, override_settings
11-
from django.http import HttpResponse, StreamingHttpResponse
11+
from django.http import HttpResponse, StreamingHttpResponse, QueryDict
1212

1313
import request_logging
1414
from request_logging.middleware import LoggingMiddleware, DEFAULT_LOG_LEVEL, DEFAULT_COLORIZE, DEFAULT_MAX_BODY_LENGTH,\
@@ -59,6 +59,7 @@ def test_no_exception_risen(self, mock_log):
5959
body = u"some body"
6060
request = self.factory.post("/a-missing-route-somewhere", data={"file": body})
6161
self.middleware.process_request(request)
62+
self.middleware.process_body(request)
6263
self._assert_logged(mock_log, body)
6364

6465

@@ -79,13 +80,15 @@ def test_request_body_logged(self, mock_log):
7980
body = u"some body"
8081
request = self.factory.post("/somewhere", data={"file": body})
8182
self.middleware.process_request(request)
83+
self.middleware.process_body(request)
8284
self._assert_logged(mock_log, body)
8385

8486
def test_request_binary_logged(self, mock_log):
8587
body = u"some body"
8688
datafile = io.StringIO(body)
8789
request = self.factory.post("/somewhere", data={"file": datafile})
8890
self.middleware.process_request(request)
91+
self.middleware.process_body(request)
8992
self._assert_logged(mock_log, "(binary data)")
9093

9194
def test_request_jpeg_logged(self, mock_log):
@@ -96,6 +99,7 @@ def test_request_jpeg_logged(self, mock_log):
9699
datafile = io.BytesIO(body)
97100
request = self.factory.post("/somewhere", data={"file": datafile})
98101
self.middleware.process_request(request)
102+
self.middleware.process_body(request)
99103
self._assert_logged(mock_log, "(multipart/form)")
100104

101105
def test_request_headers_logged(self, mock_log):
@@ -315,7 +319,7 @@ def test_default_max_body_length(self, mock_log):
315319

316320
body = DEFAULT_MAX_BODY_LENGTH * "0" + "1"
317321
request = factory.post("/somewhere", data={"file": body})
318-
middleware.process_request(request)
322+
middleware.process_body(request)
319323

320324
request_body_str = request.body if isinstance(request.body, str) else request.body.decode()
321325
self._assert_logged(mock_log, re.sub(r'\r?\n', '', request_body_str[:DEFAULT_MAX_BODY_LENGTH]))
@@ -328,7 +332,7 @@ def test_customized_max_body_length(self, mock_log):
328332

329333
body = 150 * "0" + "1"
330334
request = factory.post("/somewhere", data={"file": body})
331-
middleware.process_request(request)
335+
middleware.process_body(request)
332336

333337
request_body_str = request.body if isinstance(request.body, str) else request.body.decode()
334338
self._assert_logged(mock_log, re.sub(r'\r?\n', '', request_body_str[:150]))
@@ -400,6 +404,31 @@ def test_still_logs_path(self, mock_log):
400404
self._assert_logged(mock_log, uri)
401405

402406

407+
@mock.patch.object(request_logging.middleware, "request_logger")
408+
class DjangoDecoratorTestCase(BaseLogTestCase):
409+
def setUp(self):
410+
self.factory = RequestFactory()
411+
def get_response(request):
412+
response = mock.MagicMock()
413+
response.status_code = 200
414+
response.get.return_value = 'application/json'
415+
return response
416+
self.middleware = LoggingMiddleware(get_response)
417+
418+
# Because django isn't actually processing this test this test stubbed
419+
# out the important parts of what it does which is processes a decorator
420+
# like @sensitive_post_parameters('pass__word'), which gets added
421+
# to the request object, and then I also created the QueryDict that is
422+
# the underlying POST data.
423+
def test_log_sensitive_post_parameters(self, mock_log):
424+
uri = "/dont_log_sensitive_parameter"
425+
request = self.factory.post(uri)
426+
request.POST = QueryDict('pass_word=foo')
427+
request.sensitive_post_parameters = ["pass_word"]
428+
self.middleware.__call__(request)
429+
self._assert_not_logged(mock_log, "foo")
430+
431+
403432
@mock.patch.object(request_logging.middleware, "request_logger")
404433
class DRFTestCase(BaseLogTestCase):
405434
def setUp(self):

0 commit comments

Comments
 (0)