defcreate_signed_value(secret, name, value, version=None, clock=None): if version isNone: version = DEFAULT_SIGNED_VALUE_VERSION if clock isNone: clock = time.time timestamp = utf8(str(int(clock()))) value = base64.b64encode(utf8(value)) if version == 1: signature = _create_signature_v1(secret, name, value, timestamp) value = b"|".join([value, timestamp, signature]) return value elif version == 2: # The v2 format consists of a version number and a series of # length-prefixed fields "%d:%s", the last of which is a # signature, all separated by pipes. All numbers are in # decimal format with no leading zeros. The signature is an # HMAC-SHA256 of the whole string up to that point, including # the final pipe. # # The fields are: # - format version (i.e. 2; no length prefix) # - key version (currently 0; reserved for future key rotation features) # - timestamp (integer seconds since epoch) # - name (not encoded; assumed to be ~alphanumeric) # - value (base64-encoded) # - signature (hex-encoded; no length prefix) defformat_field(s): return utf8("%d:" % len(s)) + utf8(s) to_sign = b"|".join([ b"2|1:0", format_field(timestamp), format_field(name), format_field(value), b'']) signature = _create_signature_v2(secret, to_sign) return to_sign + signature else: raise ValueError("Unsupported version %d" % version)
def_create_signature_v1(secret, *parts): hash = hmac.new(utf8(secret), digestmod=hashlib.sha1) for part in parts: hash.update(utf8(part)) return utf8(hash.hexdigest())
defget_secure_cookie(self, name, value=None, max_age_days=31, min_version=None): """Returns the given signed cookie if it validates, or None. The decoded cookie value is returned as a byte string (unlike `get_cookie`). .. versionchanged:: 3.2.1 Added the ``min_version`` argument. Introduced cookie version 2; both versions 1 and 2 are accepted by default. """ self.require_setting("cookie_secret", "secure cookies") if value isNone: value = self.get_cookie(name) return decode_signed_value(self.application.settings["cookie_secret"], name, value, max_age_days=max_age_days, min_version=min_version)
# Figure out what version this is. Version 1 did not include an # explicit version field and started with arbitrary base64 data, # which makes this tricky. value = utf8(value) m = _signed_value_version_re.match(value) if m isNone: version = 1 else: try: version = int(m.group(1)) if version > 999: # Certain payloads from the version-less v1 format may # be parsed as valid integers. Due to base64 padding # restrictions, this can only happen for numbers whose # length is a multiple of 4, so we can treat all # numbers up to 999 as versions, and for the rest we # fall back to v1 format. version = 1 except ValueError: version = 1
if version < min_version: returnNone if version == 1: return _decode_signed_value_v1(secret, name, value, max_age_days, clock) elif version == 2: return _decode_signed_value_v2(secret, name, value, max_age_days, clock) else: returnNone
def_decode_signed_value_v1(secret, name, value, max_age_days, clock): parts = utf8(value).split(b"|") if len(parts) != 3: returnNone signature = _create_signature_v1(secret, name, parts[0], parts[1]) ifnot _time_independent_equals(parts[2], signature): gen_log.warning("Invalid cookie signature %r", value) returnNone timestamp = int(parts[1]) if timestamp < clock() - max_age_days * 86400: gen_log.warning("Expired cookie %r", value) returnNone if timestamp > clock() + 31 * 86400: # _cookie_signature does not hash a delimiter between the # parts of the cookie, so an attacker could transfer trailing # digits from the payload to the timestamp without altering the # signature. For backwards compatibility, sanity-check timestamp # here instead of modifying _cookie_signature. gen_log.warning("Cookie timestamp in future; possible tampering %r", value) returnNone if parts[1].startswith(b"0"): gen_log.warning("Tampered cookie %r", value) returnNone try: return base64.b64decode(parts[0]) except Exception: returnNone
def_decode_signed_value_v2(secret, name, value, max_age_days, clock): def_consume_field(s): length, _, rest = s.partition(b':') n = int(length) field_value = rest[:n] # In python 3, indexing bytes returns small integers; we must # use a slice to get a byte string as in python 2. if rest[n:n + 1] != b'|': raise ValueError("malformed v2 signed value field") rest = rest[n + 1:] return field_value, rest rest = value[2:] # remove version number try: key_version, rest = _consume_field(rest) timestamp, rest = _consume_field(rest) name_field, rest = _consume_field(rest) value_field, rest = _consume_field(rest) except ValueError: returnNone passed_sig = rest signed_string = value[:-len(passed_sig)] expected_sig = _create_signature_v2(secret, signed_string) ifnot _time_independent_equals(passed_sig, expected_sig): returnNone if name_field != utf8(name): returnNone timestamp = int(timestamp) if timestamp < clock() - max_age_days * 86400: # The signature has expired. returnNone try: return base64.b64decode(value_field) except Exception: returnNone
if hasattr(hmac, 'compare_digest'): # python 3.3 _time_independent_equals = hmac.compare_digest else: def_time_independent_equals(a, b): if len(a) != len(b): returnFalse result = 0 if isinstance(a[0], int): # python3 byte strings for x, y in zip(a, b): result |= x ^ y else: # python2 for x, y in zip(a, b): result |= ord(x) ^ ord(y) return result == 0
defxsrf_form_html(self): """An HTML ``<input/>`` element to be included with all POST forms. It defines the ``_xsrf`` input value, which we check on all POST requests to prevent cross-site request forgery. If you have set the ``xsrf_cookies`` application setting, you must include this HTML within all of your HTML forms. In a template, this method should be called with ``{% module xsrf_form_html() %}`` See `check_xsrf_cookie()` above for more information. """ return'<input type="hidden" name="_xsrf" value="' + \ escape.xhtml_escape(self.xsrf_token) + '"/>'
def_get_raw_xsrf_token(self): """Read or generate the xsrf token in its raw form. The raw_xsrf_token is a tuple containing: * version: the version of the cookie from which this token was read, or None if we generated a new token in this request. * token: the raw token data; random (non-ascii) bytes. * timestamp: the time this token was generated (will not be accurate for version 1 cookies) """ ifnot hasattr(self, '_raw_xsrf_token'): cookie = self.get_cookie("_xsrf") if cookie: version, token, timestamp = self._decode_xsrf_token(cookie) else: version, token, timestamp = None, None, None if token isNone: version = None token = os.urandom(16) timestamp = time.time() self._raw_xsrf_token = (version, token, timestamp) return self._raw_xsrf_token
def_decode_xsrf_token(self, cookie): """Convert a cookie string into a the tuple form returned by _get_raw_xsrf_token. """ m = _signed_value_version_re.match(utf8(cookie)) if m: version = int(m.group(1)) if version == 2: _, mask, masked_token, timestamp = cookie.split("|") mask = binascii.a2b_hex(utf8(mask)) token = _websocket_mask( mask, binascii.a2b_hex(utf8(masked_token))) timestamp = int(timestamp) return version, token, timestamp else: # Treat unknown versions as not present instead of failing. returnNone, None, None else: version = 1 try: token = binascii.a2b_hex(utf8(cookie)) except (binascii.Error, TypeError): token = utf8(cookie) # We don't have a usable timestamp in older versions. timestamp = int(time.time()) return (version, token, timestamp)
@gen.coroutine def_execute(self, transforms, *args, **kwargs): ………… # If XSRF cookies are turned on, reject form submissions without # the proper cookie if self.request.method notin ("GET", "HEAD", "OPTIONS") and \ self.application.settings.get("xsrf_cookies"): self.check_xsrf_cookie() …………
defcheck_xsrf_cookie(self): """Verifies that the ``_xsrf`` cookie matches the ``_xsrf`` argument. To prevent cross-site request forgery, we set an ``_xsrf`` cookie and include the same value as a non-cookie field with all ``POST`` requests. If the two do not match, we reject the form submission as a potential forgery. The ``_xsrf`` value may be set as either a form field named ``_xsrf`` or in a custom HTTP header named ``X-XSRFToken`` or ``X-CSRFToken`` (the latter is accepted for compatibility with Django). See http://en.wikipedia.org/wiki/Cross-site_request_forgery Prior to release 1.1.1, this check was ignored if the HTTP header ``X-Requested-With: XMLHTTPRequest`` was present. This exception has been shown to be insecure and has been removed. For more information please see http://www.djangoproject.com/weblog/2011/feb/08/security/ http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails .. versionchanged:: 3.2.2 Added support for cookie version 2. Both versions 1 and 2 are supported. """ token = (self.get_argument("_xsrf", None) or self.request.headers.get("X-Xsrftoken") or self.request.headers.get("X-Csrftoken")) ifnot token: raise HTTPError(403, "'_xsrf' argument missing from POST") _, token, _ = self._decode_xsrf_token(token) _, expected_token, _ = self._get_raw_xsrf_token() ifnot _time_independent_equals(utf8(token), utf8(expected_token)): raise HTTPError(403, "XSRF cookie does not match POST argument")