Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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

…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...
commit 78be884ea788835ad98ad433862a82cf192c3d4f 1 parent ba21814
Russell Keith-Magee authored November 26, 2010
1  django/conf/global_settings.py
@@ -421,6 +421,7 @@
421 421
 SESSION_COOKIE_DOMAIN = None                            # A string like ".lawrence.com", or None for standard domain cookie.
422 422
 SESSION_COOKIE_SECURE = False                           # Whether the session cookie should be secure (https:// only).
423 423
 SESSION_COOKIE_PATH = '/'                               # The path of the session cookie.
  424
+SESSION_COOKIE_HTTPONLY = False                         # Whether to use the non-RFC standard httpOnly flag (IE, FF3+, others)
424 425
 SESSION_SAVE_EVERY_REQUEST = False                      # Whether to save the session data on every request.
425 426
 SESSION_EXPIRE_AT_BROWSER_CLOSE = False                 # Whether a user's session cookie expires when the Web browser is closed.
426 427
 SESSION_ENGINE = 'django.contrib.sessions.backends.db'  # The module to store session data
3  django/contrib/sessions/middleware.py
@@ -38,5 +38,6 @@ def process_response(self, request, response):
38 38
                         request.session.session_key, max_age=max_age,
39 39
                         expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
40 40
                         path=settings.SESSION_COOKIE_PATH,
41  
-                        secure=settings.SESSION_COOKIE_SECURE or None)
  41
+                        secure=settings.SESSION_COOKIE_SECURE or None,
  42
+                        httponly=settings.SESSION_COOKIE_HTTPONLY or None)
42 43
         return response
44  django/contrib/sessions/tests.py
@@ -11,8 +11,10 @@
11 11
 from django.contrib.sessions.backends.file import SessionStore as FileSession
12 12
 from django.contrib.sessions.backends.base import SessionBase
13 13
 from django.contrib.sessions.models import Session
  14
+from django.contrib.sessions.middleware import SessionMiddleware
14 15
 from django.core.exceptions import ImproperlyConfigured
15  
-from django.test import TestCase
  16
+from django.http import HttpResponse
  17
+from django.test import TestCase, RequestFactory
16 18
 from django.utils import unittest
17 19
 from django.utils.hashcompat import md5_constructor
18 20
 
@@ -320,3 +322,43 @@ def test_configuration_check(self):
320 322
 class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
321 323
 
322 324
     backend = CacheSession
  325
+
  326
+
  327
+class SessionMiddlewareTests(unittest.TestCase):
  328
+    def setUp(self):
  329
+        self.old_SESSION_COOKIE_SECURE = settings.SESSION_COOKIE_SECURE
  330
+        self.old_SESSION_COOKIE_HTTPONLY = settings.SESSION_COOKIE_HTTPONLY
  331
+
  332
+    def tearDown(self):
  333
+        settings.SESSION_COOKIE_SECURE = self.old_SESSION_COOKIE_SECURE
  334
+        settings.SESSION_COOKIE_HTTPONLY = self.old_SESSION_COOKIE_HTTPONLY
  335
+
  336
+    def test_secure_session_cookie(self):
  337
+        settings.SESSION_COOKIE_SECURE = True
  338
+
  339
+        request = RequestFactory().get('/')
  340
+        response = HttpResponse('Session test')
  341
+        middleware = SessionMiddleware()
  342
+
  343
+        # Simulate a request the modifies the session
  344
+        middleware.process_request(request)
  345
+        request.session['hello'] = 'world'
  346
+
  347
+        # Handle the response through the middleware
  348
+        response = middleware.process_response(request, response)
  349
+        self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['secure'])
  350
+
  351
+    def test_httponly_session_cookie(self):
  352
+        settings.SESSION_COOKIE_HTTPONLY = True
  353
+
  354
+        request = RequestFactory().get('/')
  355
+        response = HttpResponse('Session test')
  356
+        middleware = SessionMiddleware()
  357
+
  358
+        # Simulate a request the modifies the session
  359
+        middleware.process_request(request)
  360
+        request.session['hello'] = 'world'
  361
+
  362
+        # Handle the response through the middleware
  363
+        response = middleware.process_response(request, response)
  364
+        self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['httponly'])
42  django/http/__init__.py
@@ -2,7 +2,6 @@
2 2
 import os
3 3
 import re
4 4
 import time
5  
-from Cookie import BaseCookie, SimpleCookie, CookieError
6 5
 from pprint import pformat
7 6
 from urllib import urlencode
8 7
 from urlparse import urljoin
@@ -22,6 +21,39 @@
22 21
         # PendingDeprecationWarning
23 22
         from cgi import parse_qsl
24 23
 
  24
+# httponly support exists in Python 2.6's Cookie library,
  25
+# but not in Python 2.4 or 2.5.
  26
+import Cookie
  27
+if Cookie.Morsel._reserved.has_key('httponly'):
  28
+    SimpleCookie = Cookie.SimpleCookie
  29
+else:
  30
+    class Morsel(Cookie.Morsel):
  31
+        def __setitem__(self, K, V):
  32
+            K = K.lower()
  33
+            if K == "httponly":
  34
+                if V:
  35
+                    # The superclass rejects httponly as a key,
  36
+                    # so we jump to the grandparent.
  37
+                    super(Cookie.Morsel, self).__setitem__(K, V)
  38
+            else:
  39
+                super(Morsel, self).__setitem__(K, V)
  40
+
  41
+        def OutputString(self, attrs=None):
  42
+            output = super(Morsel, self).OutputString(attrs)
  43
+            if "httponly" in self:
  44
+                output += "; httponly"
  45
+            return output
  46
+
  47
+    class SimpleCookie(Cookie.SimpleCookie):
  48
+        def __set(self, key, real_value, coded_value):
  49
+            M = self.get(key, Morsel())
  50
+            M.set(key, real_value, coded_value)
  51
+            dict.__setitem__(self, key, M)
  52
+
  53
+        def __setitem__(self, key, value):
  54
+            rval, cval = self.value_encode(value)
  55
+            self.__set(key, rval, cval)
  56
+
25 57
 from django.utils.datastructures import MultiValueDict, ImmutableList
26 58
 from django.utils.encoding import smart_str, iri_to_uri, force_unicode
27 59
 from django.utils.http import cookie_date
@@ -369,11 +401,11 @@ def value_encode(self, val):
369 401
 def parse_cookie(cookie):
370 402
     if cookie == '':
371 403
         return {}
372  
-    if not isinstance(cookie, BaseCookie):
  404
+    if not isinstance(cookie, Cookie.BaseCookie):
373 405
         try:
374 406
             c = CompatCookie()
375 407
             c.load(cookie)
376  
-        except CookieError:
  408
+        except Cookie.CookieError:
377 409
             # Invalid cookie
378 410
             return {}
379 411
     else:
@@ -462,7 +494,7 @@ def get(self, header, alternate):
462 494
         return self._headers.get(header.lower(), (None, alternate))[1]
463 495
 
464 496
     def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
465  
-                   domain=None, secure=False):
  497
+                   domain=None, secure=False, httponly=False):
466 498
         """
467 499
         Sets a cookie.
468 500
 
@@ -495,6 +527,8 @@ def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
495 527
             self.cookies[key]['domain'] = domain
496 528
         if secure:
497 529
             self.cookies[key]['secure'] = True
  530
+        if httponly:
  531
+            self.cookies[key]['httponly'] = True
498 532
 
499 533
     def delete_cookie(self, key, path='/', domain=None):
500 534
         self.set_cookie(key, max_age=0, path=path, domain=domain,
24  docs/ref/request-response.txt
@@ -566,7 +566,13 @@ Methods
566 566
     Returns ``True`` or ``False`` based on a case-insensitive check for a
567 567
     header with the given name.
568 568
 
569  
-.. method:: HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None)
  569
+.. method:: HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False)
  570
+
  571
+    .. versionchanged:: 1.3
  572
+
  573
+    The possibility of specifying a ``datetime.datetime`` object in
  574
+    ``expires``, and the auto-calculation of ``max_age`` in such case
  575
+    was added. The ``httponly`` argument was also added.
570 576
 
571 577
     Sets a cookie. The parameters are the same as in the `cookie Morsel`_
572 578
     object in the Python standard library.
@@ -583,14 +589,18 @@ Methods
583 589
           the domains www.lawrence.com, blogs.lawrence.com and
584 590
           calendars.lawrence.com. Otherwise, a cookie will only be readable by
585 591
           the domain that set it.
  592
+        * Use ``http_only=True`` if you want to prevent client-side
  593
+          JavaScript from having access to the cookie.
586 594
 
587  
-    .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
  595
+          HTTPOnly_ is a flag included in a Set-Cookie HTTP response
  596
+          header. It is not part of the RFC2109 standard for cookies,
  597
+          and it isn't honored consistently by all browsers. However,
  598
+          when it is honored, it can be a useful way to mitigate the
  599
+          risk of client side script accessing the protected cookie
  600
+          data.
588 601
 
589  
-    .. versionchanged:: 1.3
590  
-
591  
-    Both the possibility of specifying a ``datetime.datetime`` object in
592  
-    ``expires`` and the auto-calculation of ``max_age`` in such case were added
593  
-    in Django 1.3.
  602
+    .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
  603
+    .. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
594 604
 
595 605
 .. method:: HttpResponse.delete_cookie(key, path='/', domain=None)
596 606
 
19  docs/ref/settings.txt
@@ -1392,6 +1392,25 @@ The domain to use for session cookies. Set this to a string such as
1392 1392
 ``".lawrence.com"`` for cross-domain cookies, or use ``None`` for a standard
1393 1393
 domain cookie. See the :doc:`/topics/http/sessions`.
1394 1394
 
  1395
+.. setting:: SESSION_COOKIE_HTTPONLY
  1396
+
  1397
+SESSION_COOKIE_HTTPONLY
  1398
+-----------------------
  1399
+
  1400
+Default: ``False``
  1401
+
  1402
+Whether to use HTTPOnly flag on the session cookie. If this is set to
  1403
+``True``, client-side JavaScript will not to be able to access the
  1404
+session cookie.
  1405
+
  1406
+HTTPOnly_ is a flag included in a Set-Cookie HTTP response header. It
  1407
+is not part of the RFC2109 standard for cookies, and it isn't honored
  1408
+consistently by all browsers. However, when it is honored, it can be a
  1409
+useful way to mitigate the risk of client side script accessing the
  1410
+protected cookie data.
  1411
+
  1412
+.. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
  1413
+
1395 1414
 .. setting:: SESSION_COOKIE_NAME
1396 1415
 
1397 1416
 SESSION_COOKIE_NAME
4  docs/releases/1.3.txt
@@ -161,6 +161,10 @@ requests. These include:
161 161
 
162 162
     * Support for lookups spanning relations in admin's ``list_filter``.
163 163
 
  164
+    * Support for _HTTPOnly cookies.
  165
+
  166
+.. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
  167
+
164 168
 .. _backwards-incompatible-changes-1.3:
165 169
 
166 170
 Backwards-incompatible changes in 1.3
17  docs/topics/http/sessions.txt
@@ -457,6 +457,23 @@ The domain to use for session cookies. Set this to a string such as
457 457
 ``".lawrence.com"`` (note the leading dot!) for cross-domain cookies, or use
458 458
 ``None`` for a standard domain cookie.
459 459
 
  460
+SESSION_COOKIE_HTTPONLY
  461
+-----------------------
  462
+
  463
+Default: ``False``
  464
+
  465
+Whether to use HTTPOnly flag on the session cookie. If this is set to
  466
+``True``, client-side JavaScript will not to be able to access the
  467
+session cookie.
  468
+
  469
+HTTPOnly_ is a flag included in a Set-Cookie HTTP response header. It
  470
+is not part of the RFC2109 standard for cookies, and it isn't honored
  471
+consistently by all browsers. However, when it is honored, it can be a
  472
+useful way to mitigate the risk of client side script accessing the
  473
+protected cookie data.
  474
+
  475
+.. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
  476
+
460 477
 SESSION_COOKIE_NAME
461 478
 -------------------
462 479
 
9  tests/regressiontests/requests/tests.py
@@ -89,6 +89,15 @@ def test_max_age_expiration(self):
89 89
         self.assertEqual(max_age_cookie['max-age'], 10)
90 90
         self.assertEqual(max_age_cookie['expires'], cookie_date(time.time()+10))
91 91
 
  92
+    def test_httponly_cookie(self):
  93
+        response = HttpResponse()
  94
+        response.set_cookie('example', httponly=True)
  95
+        example_cookie = response.cookies['example']
  96
+        # A compat cookie may be in use -- check that it has worked
  97
+        # both as an output string, and using the cookie attributes
  98
+        self.assertTrue('; httponly' in str(example_cookie))
  99
+        self.assertTrue(example_cookie['httponly'])
  100
+
92 101
     def test_limited_stream(self):
93 102
         # Read all of a limited stream
94 103
         stream = LimitedStream(StringIO('test'), 2)

0 notes on commit 78be884

Please sign in to comment.
Something went wrong with that request. Please try again.