Commit 78be884e authored by Russell Keith-Magee's avatar Russell Keith-Magee
Browse files

Fixed #3304 -- Added support for HTTPOnly cookies. Thanks to arvin for the...

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
parent ba218145
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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
+2 −1
Original line number Diff line number Diff line
@@ -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
+43 −1
Original line number Diff line number Diff line
@@ -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'])
+38 −4
Original line number Diff line number Diff line
@@ -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,
+17 −7
Original line number Diff line number Diff line
@@ -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.

    .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
          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.

    .. 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.
    .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
    .. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly

.. method:: HttpResponse.delete_cookie(key, path='/', domain=None)

Loading