| Index: appengine/monorail/framework/xsrf.py
|
| diff --git a/appengine/monorail/framework/xsrf.py b/appengine/monorail/framework/xsrf.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..283b8ed977d8d5885071ccc443057fb1b55b2c59
|
| --- /dev/null
|
| +++ b/appengine/monorail/framework/xsrf.py
|
| @@ -0,0 +1,132 @@
|
| +# Copyright 2016 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is govered by a BSD-style
|
| +# license that can be found in the LICENSE file or at
|
| +# https://developers.google.com/open-source/licenses/bsd
|
| +
|
| +"""Utility routines for avoiding cross-site-request-forgery."""
|
| +
|
| +import base64
|
| +import hmac
|
| +import logging
|
| +import time
|
| +
|
| +# This is a file in the top-level directory that you must edit before deploying
|
| +import settings
|
| +from framework import framework_constants
|
| +from services import secrets_svc
|
| +
|
| +# This is how long tokens are valid.
|
| +TOKEN_TIMEOUT_SEC = 2 * framework_constants.SECS_PER_HOUR
|
| +
|
| +# The token refresh servlet accepts old tokens to generate new ones, but
|
| +# we still impose a limit on how old they can be.
|
| +REFRESH_TOKEN_TIMEOUT_SEC = 10 * framework_constants.SECS_PER_DAY
|
| +
|
| +# When the JS on a page decides whether or not it needs to refresh the
|
| +# XSRF token before submitting a form, there could be some clock skew,
|
| +# so we subtract a little time to avoid having the JS use an existing
|
| +# token that the server might consider expired already.
|
| +TOKEN_TIMEOUT_MARGIN_SEC = 5 * framework_constants.SECS_PER_MINUTE
|
| +
|
| +# Form tokens and issue stars are limited to only work with the specific
|
| +# servlet path for the servlet that processes them. There are several
|
| +# XHR handlers that mainly read data without making changes, so we just
|
| +# use 'xhr' with all of them.
|
| +XHR_SERVLET_PATH = 'xhr'
|
| +
|
| +DELIMITER = ':'
|
| +
|
| +
|
| +def GenerateToken(user_id, servlet_path, token_time=None):
|
| + """Return a security token specifically for the given user.
|
| +
|
| + Args:
|
| + user_id: int user ID of the user viewing an HTML form.
|
| + servlet_path: string URI path to limit the use of the token.
|
| + token_time: Time at which the token is generated in seconds since the
|
| + epoch. This is used in validation and testing. Defaults to the
|
| + current time.
|
| +
|
| + Returns:
|
| + A url-safe security token. The token is a string with the digest
|
| + the user_id and time, followed by plain-text copy of the time that is
|
| + used in validation.
|
| +
|
| + Raises:
|
| + ValueError: if the XSRF secret was not configured.
|
| + """
|
| + if not user_id:
|
| + return '' # Don't give tokens out to anonymous visitors.
|
| +
|
| + token_time = token_time or int(time.time())
|
| + digester = hmac.new(secrets_svc.GetXSRFKey())
|
| + digester.update(str(user_id))
|
| + digester.update(DELIMITER)
|
| + digester.update(servlet_path)
|
| + digester.update(DELIMITER)
|
| + digester.update(str(token_time))
|
| + digest = digester.digest()
|
| +
|
| + token = base64.urlsafe_b64encode('%s%s%d' % (digest, DELIMITER, token_time))
|
| + return token
|
| +
|
| +
|
| +def ValidateToken(
|
| + token, user_id, servlet_path, now=None, timeout=TOKEN_TIMEOUT_SEC):
|
| + """Return True if the given token is valid for the given scope.
|
| +
|
| + Args:
|
| + token: String token that was presented by the user.
|
| + user_id: int user ID.
|
| + servlet_path: string URI path to limit the use of the token.
|
| + now: Time in seconds since th epoch. Defaults to the current time.
|
| + It is explicitly specified only in tests.
|
| +
|
| + Raises:
|
| + TokenIncorrect: if the token is missing or invalid.
|
| + """
|
| + if not token:
|
| + raise TokenIncorrect('missing token')
|
| +
|
| + try:
|
| + decoded = base64.urlsafe_b64decode(str(token))
|
| + token_time = long(decoded.split(DELIMITER)[-1])
|
| + except (TypeError, ValueError):
|
| + raise TokenIncorrect('could not decode token')
|
| + now = now or int(time.time())
|
| +
|
| + # The given token should match the generated one with the same time.
|
| + expected_token = GenerateToken(user_id, servlet_path, token_time=token_time)
|
| + if len(token) != len(expected_token):
|
| + raise TokenIncorrect('presented token is wrong size')
|
| +
|
| + # Perform constant time comparison to avoid timing attacks
|
| + different = 0
|
| + for x, y in zip(token, expected_token):
|
| + different |= ord(x) ^ ord(y)
|
| + if different:
|
| + raise TokenIncorrect(
|
| + 'presented token does not match expected token: %r != %r' % (
|
| + token, expected_token))
|
| +
|
| + # We check expiration last so that we only raise the expriration error
|
| + # if the token would have otherwise been valid.
|
| + if now - token_time > timeout:
|
| + raise TokenIncorrect('token has expired')
|
| +
|
| +
|
| +def TokenExpiresSec(now=None):
|
| + """Return timestamp when current tokens will expire, minus a safety margin."""
|
| + now = now or int(time.time())
|
| + return now + TOKEN_TIMEOUT_SEC - TOKEN_TIMEOUT_MARGIN_SEC
|
| +
|
| +
|
| +class Error(Exception):
|
| + """Base class for errors from this module."""
|
| + pass
|
| +
|
| +
|
| +# Caught separately in servlet.py
|
| +class TokenIncorrect(Error):
|
| + """The POST body has an incorrect URL Command Attack token."""
|
| + pass
|
|
|