mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	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:
		| @@ -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) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user