Browse Source

refactor session cookie domain logic

cache result of session cookie domain
add warnings for session cookie domain issues
add changelog
pull/2282/head
David Lord 8 years ago
parent
commit
f75ad9fca2
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
  1. 7
      CHANGES
  2. 18
      flask/helpers.py
  3. 83
      flask/sessions.py
  4. 36
      tests/test_basic.py

7
CHANGES

@ -34,6 +34,12 @@ Major release, unreleased
type is invalid. (`#2256`_) type is invalid. (`#2256`_)
- Add ``routes`` CLI command to output routes registered on the application. - Add ``routes`` CLI command to output routes registered on the application.
(`#2259`_) (`#2259`_)
- Show warning when session cookie domain is a bare hostname or an IP
address, as these may not behave properly in some browsers, such as Chrome.
(`#2282`_)
- Allow IP address as exact session cookie domain. (`#2282`_)
- ``SESSION_COOKIE_DOMAIN`` is set if it is detected through ``SERVER_NAME``.
(`#2282`_)
.. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1489: https://github.com/pallets/flask/pull/1489
.. _#1898: https://github.com/pallets/flask/pull/1898 .. _#1898: https://github.com/pallets/flask/pull/1898
@ -43,6 +49,7 @@ Major release, unreleased
.. _#2254: https://github.com/pallets/flask/pull/2254 .. _#2254: https://github.com/pallets/flask/pull/2254
.. _#2256: https://github.com/pallets/flask/pull/2256 .. _#2256: https://github.com/pallets/flask/pull/2256
.. _#2259: https://github.com/pallets/flask/pull/2259 .. _#2259: https://github.com/pallets/flask/pull/2259
.. _#2282: https://github.com/pallets/flask/pull/2282
Version 0.12.1 Version 0.12.1
-------------- --------------

18
flask/helpers.py

@ -10,6 +10,7 @@
""" """
import os import os
import socket
import sys import sys
import pkgutil import pkgutil
import posixpath import posixpath
@ -977,22 +978,23 @@ def total_seconds(td):
""" """
return td.days * 60 * 60 * 24 + td.seconds return td.days * 60 * 60 * 24 + td.seconds
def is_ip(ip):
"""Returns the if the string received is an IP or not.
:param string: the string to check if it an IP or not def is_ip(value):
:param var_name: the name of the string that is being checked """Determine if the given string is an IP address.
:returns: True if string is an IP, False if not :param value: value to check
:rtype: boolean :type value: str
:return: True if string is an IP address
:rtype: bool
""" """
import socket
for family in (socket.AF_INET, socket.AF_INET6): for family in (socket.AF_INET, socket.AF_INET6):
try: try:
socket.inet_pton(family, ip) socket.inet_pton(family, value)
except socket.error: except socket.error:
pass pass
else: else:
return True return True
return False return False

83
flask/sessions.py

@ -11,7 +11,7 @@
import uuid import uuid
import hashlib import hashlib
from warnings import warn import warnings
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from datetime import datetime from datetime import datetime
from werkzeug.http import http_date, parse_date from werkzeug.http import http_date, parse_date
@ -201,30 +201,62 @@ class SessionInterface(object):
return isinstance(obj, self.null_session_class) return isinstance(obj, self.null_session_class)
def get_cookie_domain(self, app): def get_cookie_domain(self, app):
"""Helpful helper method that returns the cookie domain that should """Returns the domain that should be set for the session cookie.
be used for the session cookie if session cookies are used.
Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise
falls back to detecting the domain based on ``SERVER_NAME``.
Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is
updated to avoid re-running the logic.
""" """
if app.config['SESSION_COOKIE_DOMAIN'] is not None:
return app.config['SESSION_COOKIE_DOMAIN'] rv = app.config['SESSION_COOKIE_DOMAIN']
if app.config['SERVER_NAME'] is not None:
# chop off the port which is usually not supported by browsers # set explicitly, or cached from SERVER_NAME detection
rv = '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] # if False, return None
if rv is not None:
# Google chrome does not like cookies set to .localhost, so return rv if rv else None
# we just go with no domain then. Flask documents anyways that
# cross domain cookies need a fully qualified domain name rv = app.config['SERVER_NAME']
if rv == '.localhost':
rv = None # server name not set, cache False to return none next time
if not rv:
# If we infer the cookie domain from the server name we need app.config['SESSION_COOKIE_DOMAIN'] = False
# to check if we are in a subpath. In that case we can't return None
# set a cross domain cookie.
if rv is not None: # chop off the port which is usually not supported by browsers
path = self.get_cookie_path(app) # remove any leading '.' since we'll add that later
if path != '/': rv = rv.rsplit(':', 1)[0].lstrip('.')
rv = rv.lstrip('.')
if '.' not in rv:
return rv # Chrome doesn't allow names without a '.'
# this should only come up with localhost
# hack around this by not setting the name, and show a warning
warnings.warn(
'"{rv}" is not a valid cookie domain, it must contain a ".".'
' Add an entry to your hosts file, for example'
' "{rv}.localdomain", and use that instead.'.format(rv=rv)
)
app.config['SESSION_COOKIE_DOMAIN'] = False
return None
ip = is_ip(rv)
if ip:
warnings.warn(
'The session cookie domain is an IP address. This may not work'
' as intended in some browsers. Add an entry to your hosts'
' file, for example "localhost.localdomain", and use that'
' instead.'
)
# if this is not an ip and app is mounted at the root, allow subdomain
# matching by adding a '.' prefix
if self.get_cookie_path(app) == '/' and not ip:
rv = '.' + rv
app.config['SESSION_COOKIE_DOMAIN'] = rv
return rv
def get_cookie_path(self, app): def get_cookie_path(self, app):
"""Returns the path for which the cookie should be valid. The """Returns the path for which the cookie should be valid. The
@ -337,9 +369,6 @@ class SecureCookieSessionInterface(SessionInterface):
def save_session(self, app, session, response): def save_session(self, app, session, response):
domain = self.get_cookie_domain(app) domain = self.get_cookie_domain(app)
if domain is not None:
if is_ip(domain):
warnings.warn("IP introduced in SESSION_COOKIE_DOMAIN", RuntimeWarning)
path = self.get_cookie_path(app) path = self.get_cookie_path(app)
# Delete case. If there is no session we bail early. # Delete case. If there is no session we bail early.

36
tests/test_basic.py

@ -351,6 +351,42 @@ def test_session_using_session_settings():
assert 'httponly' not in cookie assert 'httponly' not in cookie
def test_session_localhost_warning(recwarn):
app = flask.Flask(__name__)
app.config.update(
SECRET_KEY='testing',
SERVER_NAME='localhost:5000',
)
@app.route('/')
def index():
flask.session['testing'] = 42
return 'testing'
rv = app.test_client().get('/', 'http://localhost:5000/')
assert 'domain' not in rv.headers['set-cookie'].lower()
w = recwarn.pop(UserWarning)
assert '"localhost" is not a valid cookie domain' in str(w.message)
def test_session_ip_warning(recwarn):
app = flask.Flask(__name__)
app.config.update(
SECRET_KEY='testing',
SERVER_NAME='127.0.0.1:5000',
)
@app.route('/')
def index():
flask.session['testing'] = 42
return 'testing'
rv = app.test_client().get('/', 'http://127.0.0.1:5000/')
assert 'domain=127.0.0.1' in rv.headers['set-cookie'].lower()
w = recwarn.pop(UserWarning)
assert 'cookie domain is an IP' in str(w.message)
def test_missing_session(): def test_missing_session():
app = flask.Flask(__name__) app = flask.Flask(__name__)

Loading…
Cancel
Save