Fixed #3304 -- Added support for HTTPOnly cookies. Thanks to arvin for the suggestion, and rodolfo for the draft patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14707 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2010-11-26 13:30:50 +00:00
parent ba21814583
commit 78be884ea7
9 changed files with 150 additions and 13 deletions

View File

@ -421,6 +421,7 @@ SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # Age of cookie, in seco
SESSION_COOKIE_DOMAIN = None # A string like ".lawrence.com", or None for standard domain cookie. SESSION_COOKIE_DOMAIN = None # A string like ".lawrence.com", or None for standard domain cookie.
SESSION_COOKIE_SECURE = False # Whether the session cookie should be secure (https:// only). SESSION_COOKIE_SECURE = False # Whether the session cookie should be secure (https:// only).
SESSION_COOKIE_PATH = '/' # The path of the session cookie. SESSION_COOKIE_PATH = '/' # The path of the session cookie.
SESSION_COOKIE_HTTPONLY = False # Whether to use the non-RFC standard httpOnly flag (IE, FF3+, others)
SESSION_SAVE_EVERY_REQUEST = False # Whether to save the session data on every request. SESSION_SAVE_EVERY_REQUEST = False # Whether to save the session data on every request.
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Whether a user's session cookie expires when the Web browser is closed. SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Whether a user's session cookie expires when the Web browser is closed.
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # The module to store session data SESSION_ENGINE = 'django.contrib.sessions.backends.db' # The module to store session data

View File

@ -38,5 +38,6 @@ class SessionMiddleware(object):
request.session.session_key, max_age=max_age, request.session.session_key, max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH, path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE or None) secure=settings.SESSION_COOKIE_SECURE or None,
httponly=settings.SESSION_COOKIE_HTTPONLY or None)
return response return response

View File

@ -11,8 +11,10 @@ from django.contrib.sessions.backends.cached_db import SessionStore as CacheDBSe
from django.contrib.sessions.backends.file import SessionStore as FileSession from django.contrib.sessions.backends.file import SessionStore as FileSession
from django.contrib.sessions.backends.base import SessionBase from django.contrib.sessions.backends.base import SessionBase
from django.contrib.sessions.models import Session from django.contrib.sessions.models import Session
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase from django.http import HttpResponse
from django.test import TestCase, RequestFactory
from django.utils import unittest from django.utils import unittest
from django.utils.hashcompat import md5_constructor from django.utils.hashcompat import md5_constructor
@ -320,3 +322,43 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase):
class CacheSessionTests(SessionTestsMixin, unittest.TestCase): class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
backend = CacheSession backend = CacheSession
class SessionMiddlewareTests(unittest.TestCase):
def setUp(self):
self.old_SESSION_COOKIE_SECURE = settings.SESSION_COOKIE_SECURE
self.old_SESSION_COOKIE_HTTPONLY = settings.SESSION_COOKIE_HTTPONLY
def tearDown(self):
settings.SESSION_COOKIE_SECURE = self.old_SESSION_COOKIE_SECURE
settings.SESSION_COOKIE_HTTPONLY = self.old_SESSION_COOKIE_HTTPONLY
def test_secure_session_cookie(self):
settings.SESSION_COOKIE_SECURE = True
request = RequestFactory().get('/')
response = HttpResponse('Session test')
middleware = SessionMiddleware()
# Simulate a request the modifies the session
middleware.process_request(request)
request.session['hello'] = 'world'
# Handle the response through the middleware
response = middleware.process_response(request, response)
self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['secure'])
def test_httponly_session_cookie(self):
settings.SESSION_COOKIE_HTTPONLY = True
request = RequestFactory().get('/')
response = HttpResponse('Session test')
middleware = SessionMiddleware()
# Simulate a request the modifies the session
middleware.process_request(request)
request.session['hello'] = 'world'
# Handle the response through the middleware
response = middleware.process_response(request, response)
self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['httponly'])

View File

@ -2,7 +2,6 @@ import datetime
import os import os
import re import re
import time import time
from Cookie import BaseCookie, SimpleCookie, CookieError
from pprint import pformat from pprint import pformat
from urllib import urlencode from urllib import urlencode
from urlparse import urljoin from urlparse import urljoin
@ -22,6 +21,39 @@ except ImportError:
# PendingDeprecationWarning # PendingDeprecationWarning
from cgi import parse_qsl from cgi import parse_qsl
# httponly support exists in Python 2.6's Cookie library,
# but not in Python 2.4 or 2.5.
import Cookie
if Cookie.Morsel._reserved.has_key('httponly'):
SimpleCookie = Cookie.SimpleCookie
else:
class Morsel(Cookie.Morsel):
def __setitem__(self, K, V):
K = K.lower()
if K == "httponly":
if V:
# The superclass rejects httponly as a key,
# so we jump to the grandparent.
super(Cookie.Morsel, self).__setitem__(K, V)
else:
super(Morsel, self).__setitem__(K, V)
def OutputString(self, attrs=None):
output = super(Morsel, self).OutputString(attrs)
if "httponly" in self:
output += "; httponly"
return output
class SimpleCookie(Cookie.SimpleCookie):
def __set(self, key, real_value, coded_value):
M = self.get(key, Morsel())
M.set(key, real_value, coded_value)
dict.__setitem__(self, key, M)
def __setitem__(self, key, value):
rval, cval = self.value_encode(value)
self.__set(key, rval, cval)
from django.utils.datastructures import MultiValueDict, ImmutableList from django.utils.datastructures import MultiValueDict, ImmutableList
from django.utils.encoding import smart_str, iri_to_uri, force_unicode from django.utils.encoding import smart_str, iri_to_uri, force_unicode
from django.utils.http import cookie_date from django.utils.http import cookie_date
@ -369,11 +401,11 @@ class CompatCookie(SimpleCookie):
def parse_cookie(cookie): def parse_cookie(cookie):
if cookie == '': if cookie == '':
return {} return {}
if not isinstance(cookie, BaseCookie): if not isinstance(cookie, Cookie.BaseCookie):
try: try:
c = CompatCookie() c = CompatCookie()
c.load(cookie) c.load(cookie)
except CookieError: except Cookie.CookieError:
# Invalid cookie # Invalid cookie
return {} return {}
else: else:
@ -462,7 +494,7 @@ class HttpResponse(object):
return self._headers.get(header.lower(), (None, alternate))[1] return self._headers.get(header.lower(), (None, alternate))[1]
def set_cookie(self, key, value='', max_age=None, expires=None, path='/', def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
domain=None, secure=False): domain=None, secure=False, httponly=False):
""" """
Sets a cookie. Sets a cookie.
@ -495,6 +527,8 @@ class HttpResponse(object):
self.cookies[key]['domain'] = domain self.cookies[key]['domain'] = domain
if secure: if secure:
self.cookies[key]['secure'] = True self.cookies[key]['secure'] = True
if httponly:
self.cookies[key]['httponly'] = True
def delete_cookie(self, key, path='/', domain=None): def delete_cookie(self, key, path='/', domain=None):
self.set_cookie(key, max_age=0, path=path, domain=domain, self.set_cookie(key, max_age=0, path=path, domain=domain,

View File

@ -566,7 +566,13 @@ Methods
Returns ``True`` or ``False`` based on a case-insensitive check for a Returns ``True`` or ``False`` based on a case-insensitive check for a
header with the given name. header with the given name.
.. method:: HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None) .. method:: HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False)
.. versionchanged:: 1.3
The possibility of specifying a ``datetime.datetime`` object in
``expires``, and the auto-calculation of ``max_age`` in such case
was added. The ``httponly`` argument was also added.
Sets a cookie. The parameters are the same as in the `cookie Morsel`_ Sets a cookie. The parameters are the same as in the `cookie Morsel`_
object in the Python standard library. object in the Python standard library.
@ -583,14 +589,18 @@ Methods
the domains www.lawrence.com, blogs.lawrence.com and the domains www.lawrence.com, blogs.lawrence.com and
calendars.lawrence.com. Otherwise, a cookie will only be readable by calendars.lawrence.com. Otherwise, a cookie will only be readable by
the domain that set it. the domain that set it.
* Use ``http_only=True`` if you want to prevent client-side
JavaScript from having access to the cookie.
HTTPOnly_ is a flag included in a Set-Cookie HTTP response
header. It is not part of the RFC2109 standard for cookies,
and it isn't honored consistently by all browsers. However,
when it is honored, it can be a useful way to mitigate the
risk of client side script accessing the protected cookie
data.
.. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
.. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
.. versionchanged:: 1.3
Both the possibility of specifying a ``datetime.datetime`` object in
``expires`` and the auto-calculation of ``max_age`` in such case were added
in Django 1.3.
.. method:: HttpResponse.delete_cookie(key, path='/', domain=None) .. method:: HttpResponse.delete_cookie(key, path='/', domain=None)

View File

@ -1392,6 +1392,25 @@ The domain to use for session cookies. Set this to a string such as
``".lawrence.com"`` for cross-domain cookies, or use ``None`` for a standard ``".lawrence.com"`` for cross-domain cookies, or use ``None`` for a standard
domain cookie. See the :doc:`/topics/http/sessions`. domain cookie. See the :doc:`/topics/http/sessions`.
.. setting:: SESSION_COOKIE_HTTPONLY
SESSION_COOKIE_HTTPONLY
-----------------------
Default: ``False``
Whether to use HTTPOnly flag on the session cookie. If this is set to
``True``, client-side JavaScript will not to be able to access the
session cookie.
HTTPOnly_ is a flag included in a Set-Cookie HTTP response header. It
is not part of the RFC2109 standard for cookies, and it isn't honored
consistently by all browsers. However, when it is honored, it can be a
useful way to mitigate the risk of client side script accessing the
protected cookie data.
.. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
.. setting:: SESSION_COOKIE_NAME .. setting:: SESSION_COOKIE_NAME
SESSION_COOKIE_NAME SESSION_COOKIE_NAME

View File

@ -161,6 +161,10 @@ requests. These include:
* Support for lookups spanning relations in admin's ``list_filter``. * Support for lookups spanning relations in admin's ``list_filter``.
* Support for _HTTPOnly cookies.
.. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
.. _backwards-incompatible-changes-1.3: .. _backwards-incompatible-changes-1.3:
Backwards-incompatible changes in 1.3 Backwards-incompatible changes in 1.3

View File

@ -457,6 +457,23 @@ The domain to use for session cookies. Set this to a string such as
``".lawrence.com"`` (note the leading dot!) for cross-domain cookies, or use ``".lawrence.com"`` (note the leading dot!) for cross-domain cookies, or use
``None`` for a standard domain cookie. ``None`` for a standard domain cookie.
SESSION_COOKIE_HTTPONLY
-----------------------
Default: ``False``
Whether to use HTTPOnly flag on the session cookie. If this is set to
``True``, client-side JavaScript will not to be able to access the
session cookie.
HTTPOnly_ is a flag included in a Set-Cookie HTTP response header. It
is not part of the RFC2109 standard for cookies, and it isn't honored
consistently by all browsers. However, when it is honored, it can be a
useful way to mitigate the risk of client side script accessing the
protected cookie data.
.. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
SESSION_COOKIE_NAME SESSION_COOKIE_NAME
------------------- -------------------

View File

@ -89,6 +89,15 @@ class RequestsTests(unittest.TestCase):
self.assertEqual(max_age_cookie['max-age'], 10) self.assertEqual(max_age_cookie['max-age'], 10)
self.assertEqual(max_age_cookie['expires'], cookie_date(time.time()+10)) self.assertEqual(max_age_cookie['expires'], cookie_date(time.time()+10))
def test_httponly_cookie(self):
response = HttpResponse()
response.set_cookie('example', httponly=True)
example_cookie = response.cookies['example']
# A compat cookie may be in use -- check that it has worked
# both as an output string, and using the cookie attributes
self.assertTrue('; httponly' in str(example_cookie))
self.assertTrue(example_cookie['httponly'])
def test_limited_stream(self): def test_limited_stream(self):
# Read all of a limited stream # Read all of a limited stream
stream = LimitedStream(StringIO('test'), 2) stream = LimitedStream(StringIO('test'), 2)