Skip to content

Commit

Permalink
Fixed #3304 -- Added support for HTTPOnly cookies. Thanks to arvin fo…
Browse files Browse the repository at this point in the history
…r the suggestion, and rodolfo for the draft patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14707 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
freakboy3742 committed Nov 26, 2010
1 parent ba21814 commit 78be884
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 13 deletions.
1 change: 1 addition & 0 deletions django/conf/global_settings.py
Expand Up @@ -421,6 +421,7 @@
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
Expand Down
3 changes: 2 additions & 1 deletion django/contrib/sessions/middleware.py
Expand Up @@ -38,5 +38,6 @@ def process_response(self, request, response):
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
44 changes: 43 additions & 1 deletion django/contrib/sessions/tests.py
Expand Up @@ -11,8 +11,10 @@
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


Expand Down Expand Up @@ -320,3 +322,43 @@ def test_configuration_check(self):
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'])
42 changes: 38 additions & 4 deletions django/http/__init__.py
Expand Up @@ -2,7 +2,6 @@
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
Expand All @@ -22,6 +21,39 @@
# 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
Expand Down Expand Up @@ -369,11 +401,11 @@ def value_encode(self, val):
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:
Expand Down Expand Up @@ -462,7 +494,7 @@ def get(self, header, alternate):
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.
Expand Down Expand Up @@ -495,6 +527,8 @@ def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
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,
Expand Down
24 changes: 17 additions & 7 deletions docs/ref/request-response.txt
Expand Up @@ -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.
Expand All @@ -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.


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

.. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
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)


Expand Down
19 changes: 19 additions & 0 deletions docs/ref/settings.txt
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/1.3.txt
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions docs/topics/http/sessions.txt
Expand Up @@ -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
------------------- -------------------


Expand Down
9 changes: 9 additions & 0 deletions tests/regressiontests/requests/tests.py
Expand Up @@ -89,6 +89,15 @@ def test_max_age_expiration(self):
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)
Expand Down

0 comments on commit 78be884

Please sign in to comment.