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
This commit is contained in:
Luke Plant 2011-05-30 22:27:47 +00:00
parent dc4c2f3add
commit 524c5fa07a
13 changed files with 453 additions and 1 deletions

View File

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

View File

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

View File

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

View File

@ -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()

View File

@ -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)

View File

@ -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:`Authentication <topics/auth>`
* :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:`Conditional content processing <topics/conditional-view-processing>`
* :doc:`Content types <ref/contrib/contenttypes>`
* :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>`
* :doc:`Cryptographic signing <topics/signing>`

126
docs/ref/clickjacking.txt Normal file
View File

@ -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

View File

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

View File

@ -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.
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/>`.

View File

@ -2023,6 +2023,17 @@ See :tfilter:`allowed date format strings <date>`. See also
:setting:`DATE_FORMAT`, :setting:`DATETIME_FORMAT`, :setting:`TIME_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
===================

View File

@ -55,6 +55,15 @@ signing in Web applications.
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``
~~~~~~~~~~~~~~~~

View File

@ -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.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):
@ -216,3 +218,47 @@ class MethodDecoratorTests(TestCase):
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)

View File

@ -5,6 +5,8 @@ import re
from django.conf import settings
from django.core import mail
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
@ -371,3 +373,125 @@ class ConditionalGetMiddlewareTest(TestCase):
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 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')