OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is govered by a BSD-style |
| 3 # license that can be found in the LICENSE file or at |
| 4 # https://developers.google.com/open-source/licenses/bsd |
| 5 |
| 6 """Utility routines for avoiding cross-site-request-forgery.""" |
| 7 |
| 8 import base64 |
| 9 import hmac |
| 10 import logging |
| 11 import time |
| 12 |
| 13 # This is a file in the top-level directory that you must edit before deploying |
| 14 import settings |
| 15 from framework import framework_constants |
| 16 from services import secrets_svc |
| 17 |
| 18 # This is how long tokens are valid. |
| 19 TOKEN_TIMEOUT_SEC = 2 * framework_constants.SECS_PER_HOUR |
| 20 |
| 21 # The token refresh servlet accepts old tokens to generate new ones, but |
| 22 # we still impose a limit on how old they can be. |
| 23 REFRESH_TOKEN_TIMEOUT_SEC = 10 * framework_constants.SECS_PER_DAY |
| 24 |
| 25 # When the JS on a page decides whether or not it needs to refresh the |
| 26 # XSRF token before submitting a form, there could be some clock skew, |
| 27 # so we subtract a little time to avoid having the JS use an existing |
| 28 # token that the server might consider expired already. |
| 29 TOKEN_TIMEOUT_MARGIN_SEC = 5 * framework_constants.SECS_PER_MINUTE |
| 30 |
| 31 # Form tokens and issue stars are limited to only work with the specific |
| 32 # servlet path for the servlet that processes them. There are several |
| 33 # XHR handlers that mainly read data without making changes, so we just |
| 34 # use 'xhr' with all of them. |
| 35 XHR_SERVLET_PATH = 'xhr' |
| 36 |
| 37 DELIMITER = ':' |
| 38 |
| 39 |
| 40 def GenerateToken(user_id, servlet_path, token_time=None): |
| 41 """Return a security token specifically for the given user. |
| 42 |
| 43 Args: |
| 44 user_id: int user ID of the user viewing an HTML form. |
| 45 servlet_path: string URI path to limit the use of the token. |
| 46 token_time: Time at which the token is generated in seconds since the |
| 47 epoch. This is used in validation and testing. Defaults to the |
| 48 current time. |
| 49 |
| 50 Returns: |
| 51 A url-safe security token. The token is a string with the digest |
| 52 the user_id and time, followed by plain-text copy of the time that is |
| 53 used in validation. |
| 54 |
| 55 Raises: |
| 56 ValueError: if the XSRF secret was not configured. |
| 57 """ |
| 58 if not user_id: |
| 59 return '' # Don't give tokens out to anonymous visitors. |
| 60 |
| 61 token_time = token_time or int(time.time()) |
| 62 digester = hmac.new(secrets_svc.GetXSRFKey()) |
| 63 digester.update(str(user_id)) |
| 64 digester.update(DELIMITER) |
| 65 digester.update(servlet_path) |
| 66 digester.update(DELIMITER) |
| 67 digester.update(str(token_time)) |
| 68 digest = digester.digest() |
| 69 |
| 70 token = base64.urlsafe_b64encode('%s%s%d' % (digest, DELIMITER, token_time)) |
| 71 return token |
| 72 |
| 73 |
| 74 def ValidateToken( |
| 75 token, user_id, servlet_path, now=None, timeout=TOKEN_TIMEOUT_SEC): |
| 76 """Return True if the given token is valid for the given scope. |
| 77 |
| 78 Args: |
| 79 token: String token that was presented by the user. |
| 80 user_id: int user ID. |
| 81 servlet_path: string URI path to limit the use of the token. |
| 82 now: Time in seconds since th epoch. Defaults to the current time. |
| 83 It is explicitly specified only in tests. |
| 84 |
| 85 Raises: |
| 86 TokenIncorrect: if the token is missing or invalid. |
| 87 """ |
| 88 if not token: |
| 89 raise TokenIncorrect('missing token') |
| 90 |
| 91 try: |
| 92 decoded = base64.urlsafe_b64decode(str(token)) |
| 93 token_time = long(decoded.split(DELIMITER)[-1]) |
| 94 except (TypeError, ValueError): |
| 95 raise TokenIncorrect('could not decode token') |
| 96 now = now or int(time.time()) |
| 97 |
| 98 # The given token should match the generated one with the same time. |
| 99 expected_token = GenerateToken(user_id, servlet_path, token_time=token_time) |
| 100 if len(token) != len(expected_token): |
| 101 raise TokenIncorrect('presented token is wrong size') |
| 102 |
| 103 # Perform constant time comparison to avoid timing attacks |
| 104 different = 0 |
| 105 for x, y in zip(token, expected_token): |
| 106 different |= ord(x) ^ ord(y) |
| 107 if different: |
| 108 raise TokenIncorrect( |
| 109 'presented token does not match expected token: %r != %r' % ( |
| 110 token, expected_token)) |
| 111 |
| 112 # We check expiration last so that we only raise the expriration error |
| 113 # if the token would have otherwise been valid. |
| 114 if now - token_time > timeout: |
| 115 raise TokenIncorrect('token has expired') |
| 116 |
| 117 |
| 118 def TokenExpiresSec(now=None): |
| 119 """Return timestamp when current tokens will expire, minus a safety margin.""" |
| 120 now = now or int(time.time()) |
| 121 return now + TOKEN_TIMEOUT_SEC - TOKEN_TIMEOUT_MARGIN_SEC |
| 122 |
| 123 |
| 124 class Error(Exception): |
| 125 """Base class for errors from this module.""" |
| 126 pass |
| 127 |
| 128 |
| 129 # Caught separately in servlet.py |
| 130 class TokenIncorrect(Error): |
| 131 """The POST body has an incorrect URL Command Attack token.""" |
| 132 pass |
OLD | NEW |