Merge lp:~canonical-isd-hackers/canonical-identity-provider/bug_553932_oops_password into lp:canonical-identity-provider/release

Proposed by Danny Tamez
Status: Superseded
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/bug_553932_oops_password
Merge into: lp:canonical-identity-provider/release
Diff against target: 226 lines (+172/-8)
2 files modified
identityprovider/middleware/exception.py (+83/-4)
identityprovider/tests/test_middleware.py (+89/-4)
To merge this branch: bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/bug_553932_oops_password
Reviewer Review Type Date Requested Status
Ricardo Kirkner (community) Needs Fixing
Review via email: mp+24904@code.launchpad.net

This proposal has been superseded by a proposal from 2010-05-07.

Description of the change

Made changes so that sensitive data (defined as any setting with PASSWORD, SECRET and now PRIVATE or any request, post var with password, secret, private in it is kept out of the oops logs)

To post a comment you must log in.
Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

Instead of duplicating django's classes and methods for this, we could just monkey-patch it. Also, we are not sanitizing local variables in frames, and sometimes sensitive data is stored locally.

What should have to be monkey-patched:

. HIDDEN_SETTINGS
. ExceptionReporter.get_traceback_frames

That way we still use django's in-place error reporting mechanism, but just extending it to be stricter.

review: Needs Fixing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'identityprovider/middleware/exception.py'
2--- identityprovider/middleware/exception.py 2010-04-21 15:29:24 +0000
3+++ identityprovider/middleware/exception.py 2010-05-07 23:35:37 +0000
4@@ -1,12 +1,18 @@
5 # Copyright 2010 Canonical Ltd. This software is licensed under the
6 # GNU Affero General Public License version 3 (see the file LICENSE).
7
8+import logging
9+import re
10 import sys
11 import traceback
12-import logging
13+
14+from django.conf import settings
15+from django.db import connection
16+from django.views import debug
17 from django.views.debug import ExceptionReporter
18-from django.db import connection
19-from django.conf import settings
20+
21+debug.HIDDEN_SETTINGS = re.compile(
22+ 'SECRET|PASSWORD|PROFANITIES_LIST|PRIVATE|secret|password|private')
23
24
25 class ConsoleExceptionMiddleware(object):
26@@ -15,15 +21,88 @@
27
28
29 class LogExceptionMiddleware(object):
30+
31+ def get_reporter(self, request, *exc_info):
32+ safe = self.sanitize_request(request)
33+ reporter = ExceptionReporter(safe, *exc_info)
34+ reporter.get_traceback_frames = self.get_traceback_frames
35+ self._get_lines_from_file = reporter._get_lines_from_file
36+ return reporter
37+
38 def process_exception(self, request, exception):
39 """Log the traceback, so that it can be kept in the oops file"""
40+ reporter = self.get_reporter(request, *sys.exc_info())
41 template_debug = settings.TEMPLATE_DEBUG
42 settings.TEMPLATE_DEBUG = True
43 try:
44- reporter = ExceptionReporter(request, *sys.exc_info())
45 logging.warn(reporter.get_traceback_html())
46 logging.warn(traceback.format_exc())
47 for query in connection.queries:
48 logging.warn("time: %(time)s sql: %(sql)s" % query)
49 finally:
50 settings.TEMPLATE_DEBUG = template_debug
51+
52+ def sanitize_request(self, request):
53+ """Remove sensitive from the request before it is displayed"""
54+ dup_post = request.POST.copy()
55+ for key in request.POST.iterkeys():
56+ if debug.HIDDEN_SETTINGS.search(key):
57+ dup_post[key] = '********'
58+ request.POST = dup_post
59+ dup_get = request.GET.copy()
60+ for key in request.GET.iterkeys():
61+ if debug.HIDDEN_SETTINGS.search(key):
62+ dup_get[key] = '********'
63+ request.GET = dup_get
64+ return request
65+
66+ def sanitize_vars(self, items):
67+ sanitized = []
68+ for item in items:
69+ if debug.HIDDEN_SETTINGS.search(item[0]):
70+ sanitized.append((item[0], '********'))
71+ else:
72+ sanitized.append(item)
73+ return sanitized
74+
75+ def get_traceback_frames(self):
76+ frames = []
77+ tb = sys.exc_info()[2]
78+ while tb is not None:
79+ # support for __traceback_hide__ which is used by a few libraries
80+ # to hide internal frames.
81+ if tb.tb_frame.f_locals.get('__traceback_hide__'):
82+ tb = tb.tb_next
83+ continue
84+ filename = tb.tb_frame.f_code.co_filename
85+ function = tb.tb_frame.f_code.co_name
86+ lineno = tb.tb_lineno - 1
87+ loader = tb.tb_frame.f_globals.get('__loader__')
88+ module_name = tb.tb_frame.f_globals.get('__name__')
89+ pre_context_lineno, pre_context, context_line, post_context = \
90+ self._get_lines_from_file(filename, lineno, 7, loader,
91+ module_name)
92+ if pre_context_lineno is not None:
93+ frames.append({
94+ 'tb': tb,
95+ 'filename': filename,
96+ 'function': function,
97+ 'lineno': lineno + 1,
98+ 'vars': self.sanitize_vars(tb.tb_frame.f_locals.items()),
99+ 'id': id(tb),
100+ 'pre_context': pre_context,
101+ 'context_line': context_line,
102+ 'post_context': post_context,
103+ 'pre_context_lineno': pre_context_lineno + 1,
104+ })
105+ tb = tb.tb_next
106+
107+ if not frames:
108+ frames = [{
109+ 'filename': '<unknown>',
110+ 'function': '?',
111+ 'lineno': '?',
112+ 'context_line': '???',
113+ }]
114+
115+ return frames
116
117=== modified file 'identityprovider/tests/test_middleware.py'
118--- identityprovider/tests/test_middleware.py 2010-04-21 15:29:24 +0000
119+++ identityprovider/tests/test_middleware.py 2010-05-07 23:35:37 +0000
120@@ -1,13 +1,17 @@
121 # Copyright 2010 Canonical Ltd. This software is licensed under the
122 # GNU Affero General Public License version 3 (see the file LICENSE).
123
124+import logging
125+import sys
126+
127+from django.conf import settings
128 from django.contrib.auth.models import User, AnonymousUser
129-
130-from identityprovider.tests.utils import BasicAccountTestCase, MockRequest
131+from identityprovider.tests.utils import (BasicAccountTestCase, MockRequest,
132+ TestCase)
133 from identityprovider.models import EmailAddress, Account
134+from identityprovider.middleware.exception import LogExceptionMiddleware
135 from identityprovider.middleware.useraccount import (
136- UserAccountConversionMiddleware
137-)
138+ UserAccountConversionMiddleware)
139
140
141 class UserAccountConversionMiddlewareTestCase(BasicAccountTestCase):
142@@ -51,3 +55,84 @@
143 self.middleware.process_request(request)
144
145 self.assertTrue(isinstance(request.user, AnonymousUser))
146+
147+
148+class ExceptionMiddlewareTestCase(TestCase):
149+
150+ POST_PASSWORD = 'P1!2@3#'
151+ POST_SECRET = 'P2@3#4$'
152+ POST_PRIVATE = 'P3#4$5%'
153+ GET_PASSWORD = 'G1!2@3#'
154+ GET_SECRET = 'G2@3#4$'
155+ GET_PRIVATE = 'G3#4$5%'
156+ POST_PUBLIC = 'Canonical Rocks!'
157+ GET_PUBLIC = 'Canonical Rocks!'
158+ asterisks = '********'
159+ PRIVATE_SETTING = '7&8*9('
160+ PUBLIC_SETTING = 'Something public'
161+ FRAME_VAR = '*8*8*8*8'
162+
163+ LOG_FILENAME = '/tmp/logging_example.out'
164+ logging.basicConfig(filename=LOG_FILENAME, level=logging.DEBUG)
165+
166+ logging.debug('This message should go to the log file')
167+
168+ def setUp(self):
169+ request = MockRequest('/login')
170+ request.POST = {}
171+ request.GET = {}
172+ request.COOKIES = {}
173+ request.META = {}
174+ request.POST['post_password'] = self.POST_PASSWORD
175+ request.POST['post_secret'] = self.POST_SECRET
176+ request.POST['post_private'] = self.POST_PRIVATE
177+ request.POST['post_public'] = self.POST_PUBLIC
178+ request.GET['get_password'] = self.GET_PASSWORD
179+ request.GET['get_secret'] = self.GET_SECRET
180+ request.GET['get_private'] = self.GET_PRIVATE
181+ request.GET['get_public'] = self.GET_PUBLIC
182+ self.request = request
183+ self.middleware = LogExceptionMiddleware()
184+ settings.PRIVATE_SETTING = self.PRIVATE_SETTING
185+ settings.PUBLIC_SETTING = self.PUBLIC_SETTING
186+
187+ def test_settings_sanitization(self):
188+ try:
189+ x = 2 / 0
190+ except:
191+ reporter = self.middleware.get_reporter(self.request,
192+ *sys.exc_info())
193+ html = reporter.get_traceback_html()
194+ self.assertFalse(self.PRIVATE_SETTING in html)
195+ self.assertTrue(self.PUBLIC_SETTING in html)
196+
197+ def test_frames_sanitization(self):
198+ try:
199+ FRAME_PASSWORD = self.FRAME_VAR
200+ public_variable = 2 / 0
201+ except:
202+ reporter = self.middleware.get_reporter(self.request,
203+ *sys.exc_info())
204+ html = reporter.get_traceback_html()
205+ self.assertFalse(self.FRAME_VAR in html)
206+ self.assertTrue('public_variable' in html)
207+
208+ def test_request_sanitization(self):
209+ req = self.middleware.sanitize_request(self.request)
210+ # Make sure sensitive data is no longer in the request
211+ self.assertFalse(self.POST_PASSWORD in req.POST.values())
212+ self.assertFalse(self.POST_SECRET in req.POST.values())
213+ self.assertFalse(self.POST_PRIVATE in req.POST.values())
214+ self.assertFalse(self.GET_PASSWORD in req.GET.values())
215+ self.assertFalse(self.GET_SECRET in req.GET.values())
216+ self.assertFalse(self.GET_PRIVATE in req.GET.values())
217+ # Make sure sensitive data is replaced by asterisks
218+ self.assertEquals(req.POST['post_password'], self.asterisks)
219+ self.assertEquals(req.POST['post_secret'], self.asterisks)
220+ self.assertEquals(req.POST['post_private'], self.asterisks)
221+ self.assertEquals(req.GET['get_password'], self.asterisks)
222+ self.assertEquals(req.GET['get_secret'], self.asterisks)
223+ self.assertEquals(req.GET['get_private'], self.asterisks)
224+ # Make sure public data is unaffected
225+ self.assertEquals(req.POST['post_public'], self.POST_PUBLIC)
226+ self.assertEquals(req.GET['get_public'], self.GET_PUBLIC)