### Eclipse Workspace Patch 1.0 #P django-the-trunk Index: tests/regressiontests/middleware/tests.py =================================================================== --- tests/regressiontests/middleware/tests.py (revision 15796) +++ tests/regressiontests/middleware/tests.py (working copy) @@ -2,6 +2,8 @@ from django.conf import settings from django.http import HttpRequest +from django.http import HttpResponse +from django.middleware.clickjacking import XFrameOptionsMiddleware from django.middleware.common import CommonMiddleware from django.middleware.http import ConditionalGetMiddleware from django.test import TestCase @@ -334,3 +336,121 @@ self.resp['Last-Modified'] = 'Sat, 12 Feb 2011 17:41:44 GMT' self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp) self.assertEqual(self.resp.status_code, 200) + +class XFrameOptionsMiddlewareTest(TestCase): + """ + Tests for the X-Frame-Options clickjacking prevention middleware. + """ + def tearDown(self): + if hasattr(settings, 'X_FRAME_OPTIONS'): + delattr(settings, 'X_FRAME_OPTIONS') + + def test_same_origin(self): + """ + Tests that the X_FRAME_OPTIONS setting can be set to SAMEORIGIN to + have the middleware use that value for the HTTP header. + """ + settings.X_FRAME_OPTIONS = 'SAMEORIGIN' + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + HttpResponse()) + self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN') + + settings.X_FRAME_OPTIONS = 'sameorigin' + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + HttpResponse()) + self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN') + + def test_deny(self): + """ + Tests that the X_FRAME_OPTIONS setting can be set to DENY to + have the middleware use that value for the HTTP header. + """ + settings.X_FRAME_OPTIONS = 'DENY' + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + HttpResponse()) + self.assertEqual(r['X-FRAME-OPTIONS'], 'DENY') + + settings.X_FRAME_OPTIONS = 'deny' + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + HttpResponse()) + self.assertEqual(r['X-FRAME-OPTIONS'], 'DENY') + + def test_defaults_sameorigin(self): + """ + Tests that if the X_FRAME_OPTIONS setting is not set then it defaults + to SAMEORIGIN. + """ + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + HttpResponse()) + self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN') + + def test_dont_set_if_set(self): + """ + Tests that if the X-FRAME-OPTIONS header is already set then the + middleware does not attempt to override it. + """ + settings.X_FRAME_OPTIONS = 'DENY' + response = HttpResponse() + response['X-FRAME-OPTIONS'] = 'SAMEORIGIN' + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + response) + self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN') + + settings.X_FRAME_OPTIONS = 'SAMEORIGIN' + response = HttpResponse() + response['X-FRAME-OPTIONS'] = 'DENY' + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + response) + self.assertEqual(r['X-FRAME-OPTIONS'], 'DENY') + + def test_response_exempt(self): + """ + Tests that if the response has a xframe_options_exempt attribute set + to False then it still sets the header, but if it's set to True then + it does not. + """ + settings.X_FRAME_OPTIONS = 'SAMEORIGIN' + response = HttpResponse() + response.xframe_options_exempt = False + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + response) + self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN') + + response = HttpResponse() + response.xframe_options_exempt = True + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + response) + self.assertEqual(r.get('X-FRAME-OPTIONS', None), None) + + def test_is_extendable(self): + """ + Tests that the XFrameOptionsMiddleware method that determines the + X-FRAME-OPTIONS header value can be overridden based on something in + the request or response. + """ + class OtherXFrameOptionsMiddleware(XFrameOptionsMiddleware): + # This is just an example for testing purposes... + def get_xframe_options_value(self, request, response): + if getattr(request, 'sameorigin', False): + return 'SAMEORIGIN' + if getattr(response, 'sameorigin', False): + return 'SAMEORIGIN' + return 'DENY' + + settings.X_FRAME_OPTIONS = 'DENY' + response = HttpResponse() + response.sameorigin = True + r = OtherXFrameOptionsMiddleware().process_response(HttpRequest(), + response) + self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN') + + request = HttpRequest() + request.sameorigin = True + r = OtherXFrameOptionsMiddleware().process_response(request, + HttpResponse()) + self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN') + + settings.X_FRAME_OPTIONS = 'SAMEORIGIN' + r = OtherXFrameOptionsMiddleware().process_response(HttpRequest(), + HttpResponse()) + self.assertEqual(r['X-FRAME-OPTIONS'], 'DENY') Index: docs/ref/contrib/index.txt =================================================================== --- docs/ref/contrib/index.txt (revision 15796) +++ docs/ref/contrib/index.txt (working copy) @@ -24,6 +24,7 @@ admin/index auth + clickjacking comments/index contenttypes csrf Index: docs/index.txt =================================================================== --- docs/index.txt (revision 15796) +++ docs/index.txt (working copy) @@ -167,8 +167,9 @@ * :doc:`Admin site ` | :doc:`Admin actions ` | :doc:`Admin documentation generator` * :doc:`Authentication ` * :doc:`Cache system ` - * :doc:`Conditional content processing ` + * :doc:`Clickjacking protection ` * :doc:`Comments ` | :doc:`Moderation ` | :doc:`Custom comments ` + * :doc:`Conditional content processing ` * :doc:`Content types ` * :doc:`Cross Site Request Forgery protection ` * :doc:`Databrowse ` Index: tests/regressiontests/decorators/tests.py =================================================================== --- tests/regressiontests/decorators/tests.py (revision 15796) +++ tests/regressiontests/decorators/tests.py (working copy) @@ -13,6 +13,8 @@ from django.views.decorators.http import require_http_methods, require_GET, require_POST from django.views.decorators.vary import vary_on_headers, vary_on_cookie from django.views.decorators.cache import cache_page, never_cache, cache_control +from django.views.decorators.clickjacking import xframe_options_deny, xframe_options_sameorigin, xframe_options_exempt +from django.middleware.clickjacking import XFrameOptionsMiddleware def fully_decorated(request): @@ -183,3 +185,46 @@ self.assertEqual(Test.method.__doc__, 'A method') self.assertEqual(Test.method.im_func.__name__, 'method') + +class XFrameOptionsDecoratorsTests(TestCase): + """ + Tests for the X-FRAME-OPTIONS decorators. + """ + def test_deny_decorator(self): + """ + Ensures @xframe_options_deny properly sets the X-FRAME-OPTIONS header. + """ + @xframe_options_deny + def a_view(request): + return HttpResponse() + r = a_view(HttpRequest()) + self.assertEqual(r['X-FRAME-OPTIONS'], 'DENY') + + def test_sameorigin_decorator(self): + """ + Ensures @xframe_options_sameorigin properly sets the X-FRAME-OPTIONS + header. + """ + @xframe_options_sameorigin + def a_view(request): + return HttpResponse() + r = a_view(HttpRequest()) + self.assertEqual(r['X-FRAME-OPTIONS'], 'SAMEORIGIN') + + def test_exempt_decorator(self): + """ + Ensures @xframe_options_exempt properly instructs the + XFrameOptionsMiddleware to NOT set the header. + """ + @xframe_options_exempt + def a_view(request): + return HttpResponse() + req = HttpRequest() + resp = a_view(req) + self.assertEqual(resp.get('X-FRAME-OPTIONS', None), None) + self.assertTrue(resp.xframe_options_exempt) + + # Since the real purpose of the exempt decorator is to suppress + # the middleware's functionality, let's make sure it actually works... + r = XFrameOptionsMiddleware().process_response(req, resp) + self.assertEqual(r.get('X-FRAME-OPTIONS', None), None) Index: django/views/decorators/clickjacking.py =================================================================== --- django/views/decorators/clickjacking.py (revision 0) +++ django/views/decorators/clickjacking.py (revision 0) @@ -0,0 +1,64 @@ +from django.utils.decorators import available_attrs + +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.4 fallback. + +def xframe_options_deny(view_func): + """ + Modifies a view function so its response has the X-FRAME-OPTIONS HTTP + header set to 'DENY' as long as the response doesn't already have that + header set. + + e.g. + + @xframe_options_deny + def some_view(request): + ... + + """ + def wrapped_view(*args, **kwargs): + resp = view_func(*args, **kwargs) + if resp.get('X-FRAME-OPTIONS', None) is None: + resp['X-FRAME-OPTIONS'] = 'DENY' + return resp + return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view) + +def xframe_options_sameorigin(view_func): + """ + Modifies a view function so its response has the X-FRAME-OPTIONS HTTP + header set to 'SAMEORIGIN' as long as the response doesn't already have + that header set. + + e.g. + + @xframe_options_sameorigin + def some_view(request): + ... + + """ + def wrapped_view(*args, **kwargs): + resp = view_func(*args, **kwargs) + if resp.get('X-FRAME-OPTIONS', None) is None: + resp['X-FRAME-OPTIONS'] = 'SAMEORIGIN' + return resp + return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view) + +def xframe_options_exempt(view_func): + """ + Modifies a view function by setting a response variable that instructs + XFrameOptionsMiddleware to NOT set the X-FRAME-OPTIONS HTTP header. + + e.g. + + @xframe_options_exempt + def some_view(request): + ... + + """ + def wrapped_view(*args, **kwargs): + resp = view_func(*args, **kwargs) + resp.xframe_options_exempt = True + return resp + return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view) Index: django/middleware/clickjacking.py =================================================================== --- django/middleware/clickjacking.py (revision 0) +++ django/middleware/clickjacking.py (revision 0) @@ -0,0 +1,51 @@ +""" +Clickjacking Protection Middleware. + +This module provides a middleware that implements protection against a +malicious site loading resources from your site in a hidden frame. +""" + +from django.conf import settings + +class XFrameOptionsMiddleware(object): + """ + Middleware that sets the X-Frame-Options HTTP header in HTTP responses. + + Does not set the header if it's already set or if the response contains + a xframe_options_exempt value set to True. + + By default, sets the X-Frame-Options header to 'SAMEORIGIN', meaning the + response can only be loaded on a frame within the same site. To prevent + the response from being loaded in a frame in any site, set X_FRAME_OPTIONS + in your project's Django settings to 'DENY'. + + Note: older browsers will quietly ignore this header, thus other + clickjacking protection techniques should be used if protection in those + browsers is required. + + http://en.wikipedia.org/wiki/Clickjacking#Server_and_client + """ + def process_response(self, request, response): + # Don't set it if it's already in the response + if response.get('X-FRAME-OPTIONS', None) is not None: + return response + + # Don't set it if they used @xframe_options_exempt + if getattr(response, 'xframe_options_exempt', False): + return response + + response['X-FRAME-OPTIONS'] = self.get_xframe_options_value(request, + response) + return response + + def get_xframe_options_value(self, request, response): + """ + Gets the value to set for the X_FRAME_OPTIONS header. + + By default uses the value from the X_FRAME_OPTIONS Django settings. If + not found in settings, defaults to 'SAMEORIGIN'. + + This method can be overridden if needing to flex based on the request + or response. + """ + return getattr(settings, 'X_FRAME_OPTIONS', 'SAMEORIGIN').upper() Index: docs/ref/contrib/clickjacking.txt =================================================================== --- docs/ref/contrib/clickjacking.txt (revision 0) +++ docs/ref/contrib/clickjacking.txt (revision 0) @@ -0,0 +1,89 @@ +======================== +Clickjacking Protection +======================== + +.. module:: django.middleware.clickjacking + :synopsis: Protects against Clickjacking + +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. + +.. _clickjacking: http://en.wikipedia.org/wiki/Clickjacking + +An example of clickjacking +========================== +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. + +Preventing clickjacking +======================= +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. + +.. _x-frame-options: https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header + +Django provides a few simple ways to include this header in responses from your site: + +1. A simple middleware that sets the header in all responses. +2. A set of view decorators that can be used to override the middleware or to only set the header for certain views. + +How to use it +============= +Setting x-frame-options for all responses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +To set the same x-frame-options value for all responses in your site, add ``'django.middleware.clickjacking.XFrameOptionsMiddleware'`` to :setting:`MIDDLEWARE_CLASSES`:: + + MIDDLEWARE_CLASSES = ( + ... + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + ... + ) + +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:: + + X_FRAME_OPTIONS = 'DENY' + +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:: + + from django.http import HttpResponse + from django.views.decorators.clickjacking import xframe_options_exempt + + @xframe_options_exempt + def ok_to_load_in_a_frame(request) + return HttpResponse("This page is safe to load in a frame on any site.") + + +Setting x-frame-options per view +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +To set the x-frame-options header on a per view basis, Django provides these decorators:: + + from django.http import HttpResponse + from django.views.decorators.clickjacking import xframe_options_deny + from django.views.decorators.clickjacking import xframe_options_sameorigin + + @xframe_options_deny + def view_one(request) + return HttpResponse("I won't display in any frame!") + + @xframe_options_sameorigin + def view_two(request) + return HttpResponse("Display in a frame if it's from the same origin as me.") + +Note that you can use the decorators in conjunction with the middleware. Use of a decorator overrides the middleware. + +Limitations +=========== +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`_. + +Browsers that support x-frame-options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Internet Explorer 8+ +* Firefox 3.6.9+ +* Opera 10.5+ +* Safari 4+ +* Chrome 4.1+ + +See also +~~~~~~~~ +A `complete list`_ of browsers supporting x-frame-options. + +.. _complete list: https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header#Browser_compatibility +.. _other clickjacking prevention techniques: http://en.wikipedia.org/wiki/Clickjacking#Prevention \ No newline at end of file