Ticket #14261: clickjacking_patch_with_docs.txt

File clickjacking_patch_with_docs.txt, 18.8 KB (added by rniemeyer, 13 years ago)

Patch, tests and docs

Line 
1### Eclipse Workspace Patch 1.0
2#P django-the-trunk
3Index: tests/regressiontests/middleware/tests.py
4===================================================================
5--- tests/regressiontests/middleware/tests.py (revision 15796)
6+++ tests/regressiontests/middleware/tests.py (working copy)
7@@ -2,6 +2,8 @@
8
9 from django.conf import settings
10 from django.http import HttpRequest
11+from django.http import HttpResponse
12+from django.middleware.clickjacking import XFrameOptionsMiddleware
13 from django.middleware.common import CommonMiddleware
14 from django.middleware.http import ConditionalGetMiddleware
15 from django.test import TestCase
16@@ -334,3 +336,121 @@
17 self.resp['Last-Modified'] = 'Sat, 12 Feb 2011 17:41:44 GMT'
18 self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
19 self.assertEqual(self.resp.status_code, 200)
20+
21+class XFrameOptionsMiddlewareTest(TestCase):
22+ """
23+ Tests for the X-Frame-Options clickjacking prevention middleware.
24+ """
25+ def tearDown(self):
26+ if hasattr(settings, 'X_FRAME_OPTIONS'):
27+ delattr(settings, 'X_FRAME_OPTIONS')
28+
29+ def test_same_origin(self):
30+ """
31+ Tests that the X_FRAME_OPTIONS setting can be set to SAMEORIGIN to
32+ have the middleware use that value for the HTTP header.
33+ """
34+ settings.X_FRAME_OPTIONS = 'SAMEORIGIN'
35+ r = XFrameOptionsMiddleware().process_response(HttpRequest(),
36+ HttpResponse())
37+ self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN')
38+
39+ settings.X_FRAME_OPTIONS = 'sameorigin'
40+ r = XFrameOptionsMiddleware().process_response(HttpRequest(),
41+ HttpResponse())
42+ self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN')
43+
44+ def test_deny(self):
45+ """
46+ Tests that the X_FRAME_OPTIONS setting can be set to DENY to
47+ have the middleware use that value for the HTTP header.
48+ """
49+ settings.X_FRAME_OPTIONS = 'DENY'
50+ r = XFrameOptionsMiddleware().process_response(HttpRequest(),
51+ HttpResponse())
52+ self.assertEqual(r['X-FRAME-OPTIONS'], 'DENY')
53+
54+ settings.X_FRAME_OPTIONS = 'deny'
55+ r = XFrameOptionsMiddleware().process_response(HttpRequest(),
56+ HttpResponse())
57+ self.assertEqual(r['X-FRAME-OPTIONS'], 'DENY')
58+
59+ def test_defaults_sameorigin(self):
60+ """
61+ Tests that if the X_FRAME_OPTIONS setting is not set then it defaults
62+ to SAMEORIGIN.
63+ """
64+ r = XFrameOptionsMiddleware().process_response(HttpRequest(),
65+ HttpResponse())
66+ self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN')
67+
68+ def test_dont_set_if_set(self):
69+ """
70+ Tests that if the X-FRAME-OPTIONS header is already set then the
71+ middleware does not attempt to override it.
72+ """
73+ settings.X_FRAME_OPTIONS = 'DENY'
74+ response = HttpResponse()
75+ response['X-FRAME-OPTIONS'] = 'SAMEORIGIN'
76+ r = XFrameOptionsMiddleware().process_response(HttpRequest(),
77+ response)
78+ self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN')
79+
80+ settings.X_FRAME_OPTIONS = 'SAMEORIGIN'
81+ response = HttpResponse()
82+ response['X-FRAME-OPTIONS'] = 'DENY'
83+ r = XFrameOptionsMiddleware().process_response(HttpRequest(),
84+ response)
85+ self.assertEqual(r['X-FRAME-OPTIONS'], 'DENY')
86+
87+ def test_response_exempt(self):
88+ """
89+ Tests that if the response has a xframe_options_exempt attribute set
90+ to False then it still sets the header, but if it's set to True then
91+ it does not.
92+ """
93+ settings.X_FRAME_OPTIONS = 'SAMEORIGIN'
94+ response = HttpResponse()
95+ response.xframe_options_exempt = False
96+ r = XFrameOptionsMiddleware().process_response(HttpRequest(),
97+ response)
98+ self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN')
99+
100+ response = HttpResponse()
101+ response.xframe_options_exempt = True
102+ r = XFrameOptionsMiddleware().process_response(HttpRequest(),
103+ response)
104+ self.assertEqual(r.get('X-FRAME-OPTIONS', None), None)
105+
106+ def test_is_extendable(self):
107+ """
108+ Tests that the XFrameOptionsMiddleware method that determines the
109+ X-FRAME-OPTIONS header value can be overridden based on something in
110+ the request or response.
111+ """
112+ class OtherXFrameOptionsMiddleware(XFrameOptionsMiddleware):
113+ # This is just an example for testing purposes...
114+ def get_xframe_options_value(self, request, response):
115+ if getattr(request, 'sameorigin', False):
116+ return 'SAMEORIGIN'
117+ if getattr(response, 'sameorigin', False):
118+ return 'SAMEORIGIN'
119+ return 'DENY'
120+
121+ settings.X_FRAME_OPTIONS = 'DENY'
122+ response = HttpResponse()
123+ response.sameorigin = True
124+ r = OtherXFrameOptionsMiddleware().process_response(HttpRequest(),
125+ response)
126+ self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN')
127+
128+ request = HttpRequest()
129+ request.sameorigin = True
130+ r = OtherXFrameOptionsMiddleware().process_response(request,
131+ HttpResponse())
132+ self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN')
133+
134+ settings.X_FRAME_OPTIONS = 'SAMEORIGIN'
135+ r = OtherXFrameOptionsMiddleware().process_response(HttpRequest(),
136+ HttpResponse())
137+ self.assertEqual(r['X-FRAME-OPTIONS'], 'DENY')
138Index: docs/ref/contrib/index.txt
139===================================================================
140--- docs/ref/contrib/index.txt (revision 15796)
141+++ docs/ref/contrib/index.txt (working copy)
142@@ -24,6 +24,7 @@
143
144 admin/index
145 auth
146+ clickjacking
147 comments/index
148 contenttypes
149 csrf
150Index: docs/index.txt
151===================================================================
152--- docs/index.txt (revision 15796)
153+++ docs/index.txt (working copy)
154@@ -167,8 +167,9 @@
155 * :doc:`Admin site <ref/contrib/admin/index>` | :doc:`Admin actions <ref/contrib/admin/actions>` | :doc:`Admin documentation generator<ref/contrib/admin/admindocs>`
156 * :doc:`Authentication <topics/auth>`
157 * :doc:`Cache system <topics/cache>`
158- * :doc:`Conditional content processing <topics/conditional-view-processing>`
159+ * :doc:`Clickjacking protection <ref/contrib/clickjacking>`
160 * :doc:`Comments <ref/contrib/comments/index>` | :doc:`Moderation <ref/contrib/comments/moderation>` | :doc:`Custom comments <ref/contrib/comments/custom>`
161+ * :doc:`Conditional content processing <topics/conditional-view-processing>`
162 * :doc:`Content types <ref/contrib/contenttypes>`
163 * :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>`
164 * :doc:`Databrowse <ref/contrib/databrowse>`
165Index: tests/regressiontests/decorators/tests.py
166===================================================================
167--- tests/regressiontests/decorators/tests.py (revision 15796)
168+++ tests/regressiontests/decorators/tests.py (working copy)
169@@ -13,6 +13,8 @@
170 from django.views.decorators.http import require_http_methods, require_GET, require_POST
171 from django.views.decorators.vary import vary_on_headers, vary_on_cookie
172 from django.views.decorators.cache import cache_page, never_cache, cache_control
173+from django.views.decorators.clickjacking import xframe_options_deny, xframe_options_sameorigin, xframe_options_exempt
174+from django.middleware.clickjacking import XFrameOptionsMiddleware
175
176
177 def fully_decorated(request):
178@@ -183,3 +185,46 @@
179
180 self.assertEqual(Test.method.__doc__, 'A method')
181 self.assertEqual(Test.method.im_func.__name__, 'method')
182+
183+class XFrameOptionsDecoratorsTests(TestCase):
184+ """
185+ Tests for the X-FRAME-OPTIONS decorators.
186+ """
187+ def test_deny_decorator(self):
188+ """
189+ Ensures @xframe_options_deny properly sets the X-FRAME-OPTIONS header.
190+ """
191+ @xframe_options_deny
192+ def a_view(request):
193+ return HttpResponse()
194+ r = a_view(HttpRequest())
195+ self.assertEqual(r['X-FRAME-OPTIONS'], 'DENY')
196+
197+ def test_sameorigin_decorator(self):
198+ """
199+ Ensures @xframe_options_sameorigin properly sets the X-FRAME-OPTIONS
200+ header.
201+ """
202+ @xframe_options_sameorigin
203+ def a_view(request):
204+ return HttpResponse()
205+ r = a_view(HttpRequest())
206+ self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN')
207+
208+ def test_exempt_decorator(self):
209+ """
210+ Ensures @xframe_options_exempt properly instructs the
211+ XFrameOptionsMiddleware to NOT set the header.
212+ """
213+ @xframe_options_exempt
214+ def a_view(request):
215+ return HttpResponse()
216+ req = HttpRequest()
217+ resp = a_view(req)
218+ self.assertEqual(resp.get('X-FRAME-OPTIONS', None), None)
219+ self.assertTrue(resp.xframe_options_exempt)
220+
221+ # Since the real purpose of the exempt decorator is to suppress
222+ # the middleware's functionality, let's make sure it actually works...
223+ r = XFrameOptionsMiddleware().process_response(req, resp)
224+ self.assertEqual(r.get('X-FRAME-OPTIONS', None), None)
225Index: django/views/decorators/clickjacking.py
226===================================================================
227--- django/views/decorators/clickjacking.py (revision 0)
228+++ django/views/decorators/clickjacking.py (revision 0)
229@@ -0,0 +1,64 @@
230+from django.utils.decorators import available_attrs
231+
232+try:
233+ from functools import wraps
234+except ImportError:
235+ from django.utils.functional import wraps # Python 2.4 fallback.
236+
237+def xframe_options_deny(view_func):
238+ """
239+ Modifies a view function so its response has the X-FRAME-OPTIONS HTTP
240+ header set to 'DENY' as long as the response doesn't already have that
241+ header set.
242+
243+ e.g.
244+
245+ @xframe_options_deny
246+ def some_view(request):
247+ ...
248+
249+ """
250+ def wrapped_view(*args, **kwargs):
251+ resp = view_func(*args, **kwargs)
252+ if resp.get('X-FRAME-OPTIONS', None) is None:
253+ resp['X-FRAME-OPTIONS'] = 'DENY'
254+ return resp
255+ return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)
256+
257+def xframe_options_sameorigin(view_func):
258+ """
259+ Modifies a view function so its response has the X-FRAME-OPTIONS HTTP
260+ header set to 'SAMEORIGIN' as long as the response doesn't already have
261+ that header set.
262+
263+ e.g.
264+
265+ @xframe_options_sameorigin
266+ def some_view(request):
267+ ...
268+
269+ """
270+ def wrapped_view(*args, **kwargs):
271+ resp = view_func(*args, **kwargs)
272+ if resp.get('X-FRAME-OPTIONS', None) is None:
273+ resp['X-FRAME-OPTIONS'] = 'SAMEORIGIN'
274+ return resp
275+ return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)
276+
277+def xframe_options_exempt(view_func):
278+ """
279+ Modifies a view function by setting a response variable that instructs
280+ XFrameOptionsMiddleware to NOT set the X-FRAME-OPTIONS HTTP header.
281+
282+ e.g.
283+
284+ @xframe_options_exempt
285+ def some_view(request):
286+ ...
287+
288+ """
289+ def wrapped_view(*args, **kwargs):
290+ resp = view_func(*args, **kwargs)
291+ resp.xframe_options_exempt = True
292+ return resp
293+ return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)
294Index: django/middleware/clickjacking.py
295===================================================================
296--- django/middleware/clickjacking.py (revision 0)
297+++ django/middleware/clickjacking.py (revision 0)
298@@ -0,0 +1,51 @@
299+"""
300+Clickjacking Protection Middleware.
301+
302+This module provides a middleware that implements protection against a
303+malicious site loading resources from your site in a hidden frame.
304+"""
305+
306+from django.conf import settings
307+
308+class XFrameOptionsMiddleware(object):
309+ """
310+ Middleware that sets the X-Frame-Options HTTP header in HTTP responses.
311+
312+ Does not set the header if it's already set or if the response contains
313+ a xframe_options_exempt value set to True.
314+
315+ By default, sets the X-Frame-Options header to 'SAMEORIGIN', meaning the
316+ response can only be loaded on a frame within the same site. To prevent
317+ the response from being loaded in a frame in any site, set X_FRAME_OPTIONS
318+ in your project's Django settings to 'DENY'.
319+
320+ Note: older browsers will quietly ignore this header, thus other
321+ clickjacking protection techniques should be used if protection in those
322+ browsers is required.
323+
324+ http://en.wikipedia.org/wiki/Clickjacking#Server_and_client
325+ """
326+ def process_response(self, request, response):
327+ # Don't set it if it's already in the response
328+ if response.get('X-FRAME-OPTIONS', None) is not None:
329+ return response
330+
331+ # Don't set it if they used @xframe_options_exempt
332+ if getattr(response, 'xframe_options_exempt', False):
333+ return response
334+
335+ response['X-FRAME-OPTIONS'] = self.get_xframe_options_value(request,
336+ response)
337+ return response
338+
339+ def get_xframe_options_value(self, request, response):
340+ """
341+ Gets the value to set for the X_FRAME_OPTIONS header.
342+
343+ By default uses the value from the X_FRAME_OPTIONS Django settings. If
344+ not found in settings, defaults to 'SAMEORIGIN'.
345+
346+ This method can be overridden if needing to flex based on the request
347+ or response.
348+ """
349+ return getattr(settings, 'X_FRAME_OPTIONS', 'SAMEORIGIN').upper()
350Index: docs/ref/contrib/clickjacking.txt
351===================================================================
352--- docs/ref/contrib/clickjacking.txt (revision 0)
353+++ docs/ref/contrib/clickjacking.txt (revision 0)
354@@ -0,0 +1,89 @@
355+========================
356+Clickjacking Protection
357+========================
358+
359+.. module:: django.middleware.clickjacking
360+ :synopsis: Protects against Clickjacking
361+
362+The clickjacking middleware and decorators provide easy-to-use protection against `clickjacking`_. This type of attack occurs when a malicious site tricks a user into clicking on a concealed element of another site which they have loaded in a hidden frame or iframe.
363+
364+.. _clickjacking: http://en.wikipedia.org/wiki/Clickjacking
365+
366+An example of clickjacking
367+==========================
368+Say an online store has a page where a logged in user can click "Buy Now" to purchase an item. A user has chosen to stay logged into the store all the time for convenience. An attacker site might try to load the store's page in a hidden iframe and overlay the "Buy Now" button with an "I Like Ponies" button. If the user visits the attacker site and clicks "I Like Ponies" he will inadvertently click on the online store's "Buy Now" button and unknowningly purchase the item.
369+
370+Preventing clickjacking
371+=======================
372+Modern browsers honor the `x-frame-options`_ HTTP header that indicates weather or not a resource is allowed to load within a frame or iframe. If the response contains the header with a value of SAMEORIGIN then the browser will only load the resource in a frame if the request originated from the same site. If the header is set to DENY then the browser will block the resource from loading in a frame no matter which site made the request.
373+
374+.. _x-frame-options: https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header
375+
376+Django provides a few simple ways to include this header in responses from your site:
377+
378+1. A simple middleware that sets the header in all responses.
379+2. A set of view decorators that can be used to override the middleware or to only set the header for certain views.
380+
381+How to use it
382+=============
383+Setting x-frame-options for all responses
384+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
385+To set the same x-frame-options value for all responses in your site, add ``'django.middleware.clickjacking.XFrameOptionsMiddleware'`` to :setting:`MIDDLEWARE_CLASSES`::
386+
387+ MIDDLEWARE_CLASSES = (
388+ ...
389+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
390+ ...
391+ )
392+
393+By default, the middleware will set the x-frame-options header to SAMEORIGIN for every outgoing ``HttpResponse``. If you want DENY instead, set the :setting:`X_FRAME_OPTIONS` setting::
394+
395+ X_FRAME_OPTIONS = 'DENY'
396+
397+When using the middleware there may be some views where you do **not** want the x-frame-options header set. For those cases, you can use a view decorator that tells the middleware to not set the header::
398+
399+ from django.http import HttpResponse
400+ from django.views.decorators.clickjacking import xframe_options_exempt
401+
402+ @xframe_options_exempt
403+ def ok_to_load_in_a_frame(request)
404+ return HttpResponse("This page is safe to load in a frame on any site.")
405+
406+
407+Setting x-frame-options per view
408+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
409+To set the x-frame-options header on a per view basis, Django provides these decorators::
410+
411+ from django.http import HttpResponse
412+ from django.views.decorators.clickjacking import xframe_options_deny
413+ from django.views.decorators.clickjacking import xframe_options_sameorigin
414+
415+ @xframe_options_deny
416+ def view_one(request)
417+ return HttpResponse("I won't display in any frame!")
418+
419+ @xframe_options_sameorigin
420+ def view_two(request)
421+ return HttpResponse("Display in a frame if it's from the same origin as me.")
422+
423+Note that you can use the decorators in conjunction with the middleware. Use of a decorator overrides the middleware.
424+
425+Limitations
426+===========
427+The `x-frame-options` header will only protect against clickjacking in a modern browser. Older browsers will quietly ignore the header and need `other clickjacking prevention techniques`_.
428+
429+Browsers that support x-frame-options
430+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
431+
432+* Internet Explorer 8+
433+* Firefox 3.6.9+
434+* Opera 10.5+
435+* Safari 4+
436+* Chrome 4.1+
437+
438+See also
439+~~~~~~~~
440+A `complete list`_ of browsers supporting x-frame-options.
441+
442+.. _complete list: https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header#Browser_compatibility
443+.. _other clickjacking prevention techniques: http://en.wikipedia.org/wiki/Clickjacking#Prevention
444\ No newline at end of file
Back to Top