浏览代码

Fixed #14261 - Added clickjacking protection (X-Frame-Options header)

Many thanks to rniemeyer for the patch!

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16298 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Luke Plant 14 年之前
父节点
当前提交
524c5fa07a

+ 1 - 0
AUTHORS

@@ -534,6 +534,7 @@ answer newbie questions, and generally made Django that much better:
     Jarek Zgoda <jarek.zgoda@gmail.com>
     Jarek Zgoda <jarek.zgoda@gmail.com>
     Cheng Zhang
     Cheng Zhang
     Zlatko Mašek <zlatko.masek@gmail.com>
     Zlatko Mašek <zlatko.masek@gmail.com>
+    Ryan Niemeyer <https://profiles.google.com/ryan.niemeyer/about>
 
 
 A big THANK YOU goes to:
 A big THANK YOU goes to:
 
 

+ 3 - 0
django/conf/global_settings.py

@@ -406,6 +406,9 @@ URL_VALIDATOR_USER_AGENT = "Django/%s (http://www.djangoproject.com)" % get_vers
 DEFAULT_TABLESPACE = ''
 DEFAULT_TABLESPACE = ''
 DEFAULT_INDEX_TABLESPACE = ''
 DEFAULT_INDEX_TABLESPACE = ''
 
 
+# Default X-Frame-Options header value
+X_FRAME_OPTIONS = 'SAMEORIGIN'
+
 ##############
 ##############
 # MIDDLEWARE #
 # MIDDLEWARE #
 ##############
 ##############

+ 2 - 0
django/conf/project_template/settings.py

@@ -98,6 +98,8 @@ MIDDLEWARE_CLASSES = (
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
+    # Uncomment the next line for simple clickjacking protection:
+    # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 )
 )
 
 
 ROOT_URLCONF = '{{ project_name }}.urls'
 ROOT_URLCONF = '{{ project_name }}.urls'

+ 51 - 0
django/middleware/clickjacking.py

@@ -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 this uses the value from the X_FRAME_OPTIONS Django
+        settings. If not found in settings, defaults to 'SAMEORIGIN'.
+
+        This method can be overridden if needed, allowing it to vary based on
+        the request or response.
+        """
+        return getattr(settings, 'X_FRAME_OPTIONS', 'SAMEORIGIN').upper()

+ 64 - 0
django/views/decorators/clickjacking.py

@@ -0,0 +1,64 @@
+from functools import wraps
+
+from django.utils.decorators import available_attrs
+
+
+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)

+ 2 - 1
docs/index.txt

@@ -167,8 +167,9 @@ Other batteries included
     * :doc:`Admin site <ref/contrib/admin/index>` | :doc:`Admin actions <ref/contrib/admin/actions>` | :doc:`Admin documentation generator<ref/contrib/admin/admindocs>`
     * :doc:`Admin site <ref/contrib/admin/index>` | :doc:`Admin actions <ref/contrib/admin/actions>` | :doc:`Admin documentation generator<ref/contrib/admin/admindocs>`
     * :doc:`Authentication <topics/auth>`
     * :doc:`Authentication <topics/auth>`
     * :doc:`Cache system <topics/cache>`
     * :doc:`Cache system <topics/cache>`
-    * :doc:`Conditional content processing <topics/conditional-view-processing>`
+    * :doc:`Clickjacking protection <ref/clickjacking>`
     * :doc:`Comments <ref/contrib/comments/index>` | :doc:`Moderation <ref/contrib/comments/moderation>` | :doc:`Custom comments <ref/contrib/comments/custom>`
     * :doc:`Comments <ref/contrib/comments/index>` | :doc:`Moderation <ref/contrib/comments/moderation>` | :doc:`Custom comments <ref/contrib/comments/custom>`
+    * :doc:`Conditional content processing <topics/conditional-view-processing>`
     * :doc:`Content types <ref/contrib/contenttypes>`
     * :doc:`Content types <ref/contrib/contenttypes>`
     * :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>`
     * :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>`
     * :doc:`Cryptographic signing <topics/signing>`
     * :doc:`Cryptographic signing <topics/signing>`

+ 126 - 0
docs/ref/clickjacking.txt

@@ -0,0 +1,126 @@
+========================
+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.
+
+.. versionadded:: 1.4
+   The clickjacking middleware and decorators were added.
+
+.. _clickjacking: http://en.wikipedia.org/wiki/Clickjacking
+
+An example of clickjacking
+==========================
+
+Suppose 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 create an "I Like Ponies" button on one
+of their own pages, and load the store's page in a transparent iframe such that
+the "Buy Now" button is invisibly overlaid on the "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 whether
+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

+ 1 - 0
docs/ref/index.txt

@@ -6,6 +6,7 @@ API Reference
    :maxdepth: 1
    :maxdepth: 1
 
 
    authbackends
    authbackends
+   clickjacking
    contrib/index
    contrib/index
    databases
    databases
    django-admin
    django-admin

+ 13 - 0
docs/ref/middleware.txt

@@ -204,3 +204,16 @@ Middleware modules running inside it (coming later in the stack) will be under
 the same transaction control as the view functions.
 the same transaction control as the view functions.
 
 
 See the :doc:`transaction management documentation </topics/db/transactions>`.
 See the :doc:`transaction management documentation </topics/db/transactions>`.
+
+X-Frame-Options middleware
+--------------------------
+
+.. module:: django.middleware.clickjacking
+   :synopsis: Clickjacking protection
+
+.. class:: XFrameOptionsMiddleware
+
+.. versionadded:: 1.4
+   ``XFrameOptionsMiddleware`` was added.
+
+Simple :doc:`clickjacking protection via the X-Frame-Options header </ref/clickjacking/>`.

+ 11 - 0
docs/ref/settings.txt

@@ -2023,6 +2023,17 @@ See :tfilter:`allowed date format strings <date>`. See also
 :setting:`DATE_FORMAT`, :setting:`DATETIME_FORMAT`, :setting:`TIME_FORMAT`
 :setting:`DATE_FORMAT`, :setting:`DATETIME_FORMAT`, :setting:`TIME_FORMAT`
 and :setting:`MONTH_DAY_FORMAT`.
 and :setting:`MONTH_DAY_FORMAT`.
 
 
+.. setting:: X_FRAME_OPTIONS
+
+X_FRAME_OPTIONS
+---------------
+
+Default: ``'SAMEORIGIN'``
+
+The default value for the X-Frame-Options header used by
+:class:`~django.middleware.clickjacking.XFrameOptionsMiddleware`. See the
+:doc:`clickjacking protection </ref/clickjacking/>` documentation.
+
 Deprecated settings
 Deprecated settings
 ===================
 ===================
 
 

+ 9 - 0
docs/releases/1.4.txt

@@ -55,6 +55,15 @@ signing in Web applications.
 
 
 See :doc:`cryptographic signing </topics/signing>` docs for more information.
 See :doc:`cryptographic signing </topics/signing>` docs for more information.
 
 
+Simple clickjacking protection
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We've added a middleware to provide easy protection against `clickjacking
+<http://en.wikipedia.org/wiki/Clickjacking>`_ using the X-Frame-Options
+header. It's not enabled by default for backwards compatibility reasons, but
+you'll almost certainly want to :doc:`enable it </ref/clickjacking/>` to help
+plug that security hole for browsers that support the header.
+
 ``reverse_lazy``
 ``reverse_lazy``
 ~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~
 
 

+ 46 - 0
tests/regressiontests/decorators/tests.py

@@ -9,6 +9,8 @@ from django.utils.unittest import TestCase
 from django.views.decorators.http import require_http_methods, require_GET, require_POST, require_safe
 from django.views.decorators.http import require_http_methods, require_GET, require_POST, require_safe
 from django.views.decorators.vary import vary_on_headers, vary_on_cookie
 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.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):
 def fully_decorated(request):
@@ -216,3 +218,47 @@ class MethodDecoratorTests(TestCase):
 
 
         self.assertEqual(Test.method.__doc__, 'A method')
         self.assertEqual(Test.method.__doc__, 'A method')
         self.assertEqual(Test.method.im_func.__name__, '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)

+ 124 - 0
tests/regressiontests/middleware/tests.py

@@ -5,6 +5,8 @@ import re
 from django.conf import settings
 from django.conf import settings
 from django.core import mail
 from django.core import mail
 from django.http import HttpRequest
 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.common import CommonMiddleware
 from django.middleware.http import ConditionalGetMiddleware
 from django.middleware.http import ConditionalGetMiddleware
 from django.test import TestCase
 from django.test import TestCase
@@ -371,3 +373,125 @@ class ConditionalGetMiddlewareTest(TestCase):
         self.resp['Last-Modified'] = 'Sat, 12 Feb 2011 17:41:44 GMT'
         self.resp['Last-Modified'] = 'Sat, 12 Feb 2011 17:41:44 GMT'
         self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
         self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
         self.assertEqual(self.resp.status_code, 200)
         self.assertEqual(self.resp.status_code, 200)
+
+
+class XFrameOptionsMiddlewareTest(TestCase):
+    """
+    Tests for the X-Frame-Options clickjacking prevention middleware.
+    """
+    def setUp(self):
+        self.x_frame_options = settings.X_FRAME_OPTIONS
+
+    def tearDown(self):
+        settings.X_FRAME_OPTIONS = self.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.
+        """
+        del settings.X_FRAME_OPTIONS
+        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')