blob: bc5ae33c2167c352d7ed70378511afa9d76a146d [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2016 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5"""Utility routines for avoiding cross-site-request-forgery."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
10import base64
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010011import hashlib
Copybara854996b2021-09-07 19:36:02 +000012import hmac
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010013import six
Copybara854996b2021-09-07 19:36:02 +000014import time
15
16# This is a file in the top-level directory that you must edit before deploying
Copybara854996b2021-09-07 19:36:02 +000017from framework import framework_constants
18from services import secrets_svc
19
20# This is how long tokens are valid.
21TOKEN_TIMEOUT_SEC = 2 * framework_constants.SECS_PER_HOUR
22
23# The token refresh servlet accepts old tokens to generate new ones, but
24# we still impose a limit on how old they can be.
25REFRESH_TOKEN_TIMEOUT_SEC = 10 * framework_constants.SECS_PER_DAY
26
27# When the JS on a page decides whether or not it needs to refresh the
28# XSRF token before submitting a form, there could be some clock skew,
29# so we subtract a little time to avoid having the JS use an existing
30# token that the server might consider expired already.
31TOKEN_TIMEOUT_MARGIN_SEC = 5 * framework_constants.SECS_PER_MINUTE
32
33# When checking that the token is not from the future, allow a little
34# margin for the possibliity that the clock of the GAE instance that
35# generated the token could be a little ahead of the one checking.
36CLOCK_SKEW_SEC = 5
37
38# Form tokens and issue stars are limited to only work with the specific
39# servlet path for the servlet that processes them. There are several
40# XHR handlers that mainly read data without making changes, so we just
41# use 'xhr' with all of them.
42XHR_SERVLET_PATH = 'xhr'
43
44
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010045DELIMITER = b':'
Copybara854996b2021-09-07 19:36:02 +000046
47
48def GenerateToken(user_id, servlet_path, token_time=None):
49 """Return a security token specifically for the given user.
50
51 Args:
52 user_id: int user ID of the user viewing an HTML form.
53 servlet_path: string URI path to limit the use of the token.
54 token_time: Time at which the token is generated in seconds since the epoch.
55
56 Returns:
57 A url-safe security token. The token is a string with the digest
58 the user_id and time, followed by plain-text copy of the time that is
59 used in validation.
60
61 Raises:
62 ValueError: if the XSRF secret was not configured.
63 """
64 token_time = token_time or int(time.time())
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010065 digester = hmac.new(secrets_svc.GetXSRFKey(), digestmod=hashlib.md5)
66 digester.update(six.ensure_binary(str(user_id)))
Copybara854996b2021-09-07 19:36:02 +000067 digester.update(DELIMITER)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010068 digester.update(six.ensure_binary(servlet_path))
Copybara854996b2021-09-07 19:36:02 +000069 digester.update(DELIMITER)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010070 digester.update(six.ensure_binary(str(token_time)))
Copybara854996b2021-09-07 19:36:02 +000071 digest = digester.digest()
72
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010073 token = base64.urlsafe_b64encode(b'%s%s%d' % (digest, DELIMITER, token_time))
74 return six.ensure_str(token)
Copybara854996b2021-09-07 19:36:02 +000075
76
77def ValidateToken(
78 token, user_id, servlet_path, timeout=TOKEN_TIMEOUT_SEC):
79 """Return True if the given token is valid for the given scope.
80
81 Args:
82 token: String token that was presented by the user.
83 user_id: int user ID.
84 servlet_path: string URI path to limit the use of the token.
85
86 Raises:
87 TokenIncorrect: if the token is missing or invalid.
88 """
89 if not token:
90 raise TokenIncorrect('missing token')
91
92 try:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010093 decoded = base64.urlsafe_b64decode(six.ensure_binary(token))
Copybara854996b2021-09-07 19:36:02 +000094 token_time = int(decoded.split(DELIMITER)[-1])
95 except (TypeError, ValueError):
96 raise TokenIncorrect('could not decode token')
97 now = int(time.time())
98
99 # The given token should match the generated one with the same time.
100 expected_token = GenerateToken(user_id, servlet_path, token_time=token_time)
101 if len(token) != len(expected_token):
102 raise TokenIncorrect('presented token is wrong size')
103
104 # Perform constant time comparison to avoid timing attacks
105 different = 0
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100106 # In Python 3, zip(bytes, bytes) gives ints, but in Python 2,
107 # zip(str, str) gives strs. We need to call ord() in Python 2 only.
108 if isinstance(token, six.string_types):
109 token = list(map(ord, token))
110 if isinstance(expected_token, six.string_types):
111 expected_token = list(map(ord, expected_token))
Copybara854996b2021-09-07 19:36:02 +0000112 for x, y in zip(token, expected_token):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100113 different |= x ^ y
Copybara854996b2021-09-07 19:36:02 +0000114 if different:
115 raise TokenIncorrect(
116 'presented token does not match expected token: %r != %r' % (
117 token, expected_token))
118
119 # We reject tokens from the future.
120 if token_time > now + CLOCK_SKEW_SEC:
121 raise TokenIncorrect('token is from future')
122
123 # We check expiration last so that we only raise the expriration error
124 # if the token would have otherwise been valid.
125 if now - token_time > timeout:
126 raise TokenIncorrect('token has expired')
127
128
129def TokenExpiresSec():
130 """Return timestamp when current tokens will expire, minus a safety margin."""
131 now = int(time.time())
132 return now + TOKEN_TIMEOUT_SEC - TOKEN_TIMEOUT_MARGIN_SEC
133
134
135class Error(Exception):
136 """Base class for errors from this module."""
137 pass
138
139
140# Caught separately in servlet.py
141class TokenIncorrect(Error):
142 """The POST body has an incorrect URL Command Attack token."""
143 pass