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

Proposed by Danny Tamez
Status: Merged
Merged at revision: 9
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/bug_553932_oops_password
Merge into: lp:canonical-identity-provider/release
Diff against target: 228 lines (+174/-8)
2 files modified
identityprovider/middleware/exception.py (+85/-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) Approve
Review via email: mp+24935@code.launchpad.net

This proposal supersedes 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 : Posted in a previous version of this proposal

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
Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

All changes discussed and agreed. Thanks.

review: Approve

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-10 15:28:26 +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,90 @@
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+ # This function was taken from the django project.
76+ # Please see the license file in the third-party/django directory.
77+ def get_traceback_frames(self):
78+ frames = []
79+ tb = sys.exc_info()[2]
80+ while tb is not None:
81+ # support for __traceback_hide__ which is used by a few libraries
82+ # to hide internal frames.
83+ if tb.tb_frame.f_locals.get('__traceback_hide__'):
84+ tb = tb.tb_next
85+ continue
86+ filename = tb.tb_frame.f_code.co_filename
87+ function = tb.tb_frame.f_code.co_name
88+ lineno = tb.tb_lineno - 1
89+ loader = tb.tb_frame.f_globals.get('__loader__')
90+ module_name = tb.tb_frame.f_globals.get('__name__')
91+ pre_context_lineno, pre_context, context_line, post_context = \
92+ self._get_lines_from_file(filename, lineno, 7, loader,
93+ module_name)
94+ if pre_context_lineno is not None:
95+ frames.append({
96+ 'tb': tb,
97+ 'filename': filename,
98+ 'function': function,
99+ 'lineno': lineno + 1,
100+ 'vars': self.sanitize_vars(tb.tb_frame.f_locals.items()),
101+ 'id': id(tb),
102+ 'pre_context': pre_context,
103+ 'context_line': context_line,
104+ 'post_context': post_context,
105+ 'pre_context_lineno': pre_context_lineno + 1,
106+ })
107+ tb = tb.tb_next
108+
109+ if not frames:
110+ frames = [{
111+ 'filename': '<unknown>',
112+ 'function': '?',
113+ 'lineno': '?',
114+ 'context_line': '???',
115+ }]
116+
117+ return frames
118
119=== modified file 'identityprovider/tests/test_middleware.py'
120--- identityprovider/tests/test_middleware.py 2010-04-21 15:29:24 +0000
121+++ identityprovider/tests/test_middleware.py 2010-05-10 15:28:26 +0000
122@@ -1,13 +1,17 @@
123 # Copyright 2010 Canonical Ltd. This software is licensed under the
124 # GNU Affero General Public License version 3 (see the file LICENSE).
125
126+import logging
127+import sys
128+
129+from django.conf import settings
130 from django.contrib.auth.models import User, AnonymousUser
131-
132-from identityprovider.tests.utils import BasicAccountTestCase, MockRequest
133+from identityprovider.tests.utils import (BasicAccountTestCase, MockRequest,
134+ TestCase)
135 from identityprovider.models import EmailAddress, Account
136+from identityprovider.middleware.exception import LogExceptionMiddleware
137 from identityprovider.middleware.useraccount import (
138- UserAccountConversionMiddleware
139-)
140+ UserAccountConversionMiddleware)
141
142
143 class UserAccountConversionMiddlewareTestCase(BasicAccountTestCase):
144@@ -51,3 +55,84 @@
145 self.middleware.process_request(request)
146
147 self.assertTrue(isinstance(request.user, AnonymousUser))
148+
149+
150+class ExceptionMiddlewareTestCase(TestCase):
151+
152+ POST_PASSWORD = 'P1!2@3#'
153+ POST_SECRET = 'P2@3#4$'
154+ POST_PRIVATE = 'P3#4$5%'
155+ GET_PASSWORD = 'G1!2@3#'
156+ GET_SECRET = 'G2@3#4$'
157+ GET_PRIVATE = 'G3#4$5%'
158+ POST_PUBLIC = 'Canonical Rocks!'
159+ GET_PUBLIC = 'Canonical Rocks!'
160+ asterisks = '********'
161+ PRIVATE_SETTING = '7&8*9('
162+ PUBLIC_SETTING = 'Something public'
163+ FRAME_VAR = '*8*8*8*8'
164+
165+ LOG_FILENAME = '/tmp/logging_example.out'
166+ logging.basicConfig(filename=LOG_FILENAME, level=logging.DEBUG)
167+
168+ logging.debug('This message should go to the log file')
169+
170+ def setUp(self):
171+ request = MockRequest('/login')
172+ request.POST = {}
173+ request.GET = {}
174+ request.COOKIES = {}
175+ request.META = {}
176+ request.POST['post_password'] = self.POST_PASSWORD
177+ request.POST['post_secret'] = self.POST_SECRET
178+ request.POST['post_private'] = self.POST_PRIVATE
179+ request.POST['post_public'] = self.POST_PUBLIC
180+ request.GET['get_password'] = self.GET_PASSWORD
181+ request.GET['get_secret'] = self.GET_SECRET
182+ request.GET['get_private'] = self.GET_PRIVATE
183+ request.GET['get_public'] = self.GET_PUBLIC
184+ self.request = request
185+ self.middleware = LogExceptionMiddleware()
186+ settings.PRIVATE_SETTING = self.PRIVATE_SETTING
187+ settings.PUBLIC_SETTING = self.PUBLIC_SETTING
188+
189+ def test_settings_sanitization(self):
190+ try:
191+ x = 2 / 0
192+ except:
193+ reporter = self.middleware.get_reporter(self.request,
194+ *sys.exc_info())
195+ html = reporter.get_traceback_html()
196+ self.assertFalse(self.PRIVATE_SETTING in html)
197+ self.assertTrue(self.PUBLIC_SETTING in html)
198+
199+ def test_frames_sanitization(self):
200+ try:
201+ FRAME_PASSWORD = self.FRAME_VAR
202+ public_variable = 2 / 0
203+ except:
204+ reporter = self.middleware.get_reporter(self.request,
205+ *sys.exc_info())
206+ html = reporter.get_traceback_html()
207+ self.assertFalse(self.FRAME_VAR in html)
208+ self.assertTrue('public_variable' in html)
209+
210+ def test_request_sanitization(self):
211+ req = self.middleware.sanitize_request(self.request)
212+ # Make sure sensitive data is no longer in the request
213+ self.assertFalse(self.POST_PASSWORD in req.POST.values())
214+ self.assertFalse(self.POST_SECRET in req.POST.values())
215+ self.assertFalse(self.POST_PRIVATE in req.POST.values())
216+ self.assertFalse(self.GET_PASSWORD in req.GET.values())
217+ self.assertFalse(self.GET_SECRET in req.GET.values())
218+ self.assertFalse(self.GET_PRIVATE in req.GET.values())
219+ # Make sure sensitive data is replaced by asterisks
220+ self.assertEquals(req.POST['post_password'], self.asterisks)
221+ self.assertEquals(req.POST['post_secret'], self.asterisks)
222+ self.assertEquals(req.POST['post_private'], self.asterisks)
223+ self.assertEquals(req.GET['get_password'], self.asterisks)
224+ self.assertEquals(req.GET['get_secret'], self.asterisks)
225+ self.assertEquals(req.GET['get_private'], self.asterisks)
226+ # Make sure public data is unaffected
227+ self.assertEquals(req.POST['post_public'], self.POST_PUBLIC)
228+ self.assertEquals(req.GET['get_public'], self.GET_PUBLIC)