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:
parent
ba21814583
commit
78be884ea7
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue