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_SECURE = False                           # Whether the session cookie should be secure (https:// only). | ||||
| 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_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 | ||||
|   | ||||
| @@ -38,5 +38,6 @@ class SessionMiddleware(object): | ||||
|                         request.session.session_key, max_age=max_age, | ||||
|                         expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, | ||||
|                         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 | ||||
|   | ||||
| @@ -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.base import SessionBase | ||||
| from django.contrib.sessions.models import Session | ||||
| from django.contrib.sessions.middleware import SessionMiddleware | ||||
| 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.hashcompat import md5_constructor | ||||
|  | ||||
| @@ -320,3 +322,43 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase): | ||||
| class CacheSessionTests(SessionTestsMixin, unittest.TestCase): | ||||
|  | ||||
|     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 re | ||||
| import time | ||||
| from Cookie import BaseCookie, SimpleCookie, CookieError | ||||
| from pprint import pformat | ||||
| from urllib import urlencode | ||||
| from urlparse import urljoin | ||||
| @@ -22,6 +21,39 @@ except ImportError: | ||||
|         # PendingDeprecationWarning | ||||
|         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.encoding import smart_str, iri_to_uri, force_unicode | ||||
| from django.utils.http import cookie_date | ||||
| @@ -369,11 +401,11 @@ class CompatCookie(SimpleCookie): | ||||
| def parse_cookie(cookie): | ||||
|     if cookie == '': | ||||
|         return {} | ||||
|     if not isinstance(cookie, BaseCookie): | ||||
|     if not isinstance(cookie, Cookie.BaseCookie): | ||||
|         try: | ||||
|             c = CompatCookie() | ||||
|             c.load(cookie) | ||||
|         except CookieError: | ||||
|         except Cookie.CookieError: | ||||
|             # Invalid cookie | ||||
|             return {} | ||||
|     else: | ||||
| @@ -462,7 +494,7 @@ class HttpResponse(object): | ||||
|         return self._headers.get(header.lower(), (None, alternate))[1] | ||||
|  | ||||
|     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. | ||||
|  | ||||
| @@ -495,6 +527,8 @@ class HttpResponse(object): | ||||
|             self.cookies[key]['domain'] = domain | ||||
|         if secure: | ||||
|             self.cookies[key]['secure'] = True | ||||
|         if httponly: | ||||
|             self.cookies[key]['httponly'] = True | ||||
|  | ||||
|     def delete_cookie(self, key, path='/', domain=None): | ||||
|         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 | ||||
|     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`_ | ||||
|     object in the Python standard library. | ||||
| @@ -583,14 +589,18 @@ Methods | ||||
|           the domains www.lawrence.com, blogs.lawrence.com and | ||||
|           calendars.lawrence.com. Otherwise, a cookie will only be readable by | ||||
|           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 | ||||
|  | ||||
|     .. 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. | ||||
|     .. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly | ||||
|  | ||||
| .. 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 | ||||
| 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 | ||||
|  | ||||
| SESSION_COOKIE_NAME | ||||
|   | ||||
| @@ -161,6 +161,10 @@ requests. These include: | ||||
|  | ||||
|     * 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 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 | ||||
| ``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 | ||||
| ------------------- | ||||
|  | ||||
|   | ||||
| @@ -89,6 +89,15 @@ class RequestsTests(unittest.TestCase): | ||||
|         self.assertEqual(max_age_cookie['max-age'], 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): | ||||
|         # Read all of a limited stream | ||||
|         stream = LimitedStream(StringIO('test'), 2) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user