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