Skip to content

Commit 2844ce9

Browse files
fix: Accepting time-zone aware expiration datetime only to prevent hard-to debug issues with invalid signatures (#12704)
* Accepting time-zone aware expiration datetime only to prevent hard-to debug issues with invalid signatures. * Applying code review suggestion * Fixing linter issues
1 parent 6f7c2d8 commit 2844ce9

File tree

2 files changed

+73
-18
lines changed

2 files changed

+73
-18
lines changed

cdn/snippets.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
# [START cloudcdn_sign_cookie]
2727
import argparse
2828
import base64
29-
from datetime import datetime
29+
from datetime import datetime, timezone
3030
import hashlib
3131
import hmac
3232
from urllib.parse import parse_qs, urlsplit
@@ -48,7 +48,7 @@ def sign_url(
4848
url: URL to sign.
4949
key_name: name of the signing key.
5050
base64_key: signing key as a base64 encoded string.
51-
expiration_time: expiration time.
51+
expiration_time: expiration time as time-zone aware datetime.
5252
5353
Returns:
5454
Returns the Signed URL appended with the query parameters based on the
@@ -57,7 +57,7 @@ def sign_url(
5757
stripped_url = url.strip()
5858
parsed_url = urlsplit(stripped_url)
5959
query_params = parse_qs(parsed_url.query, keep_blank_values=True)
60-
epoch = datetime.utcfromtimestamp(0)
60+
epoch = datetime.fromtimestamp(0, timezone.utc)
6161
expiration_timestamp = int((expiration_time - epoch).total_seconds())
6262
decoded_key = base64.urlsafe_b64decode(base64_key)
6363

@@ -87,7 +87,7 @@ def sign_url_prefix(
8787
url_prefix: URL prefix to sign.
8888
key_name: name of the signing key.
8989
base64_key: signing key as a base64 encoded string.
90-
expiration_time: expiration time.
90+
expiration_time: expiration time as time-zone aware datetime.
9191
9292
Returns:
9393
Returns the Signed URL appended with the query parameters based on the
@@ -99,7 +99,7 @@ def sign_url_prefix(
9999
encoded_url_prefix = base64.urlsafe_b64encode(
100100
url_prefix.strip().encode("utf-8")
101101
).decode("utf-8")
102-
epoch = datetime.utcfromtimestamp(0)
102+
epoch = datetime.fromtimestamp(0, timezone.utc)
103103
expiration_timestamp = int((expiration_time - epoch).total_seconds())
104104
decoded_key = base64.urlsafe_b64decode(base64_key)
105105

@@ -127,15 +127,15 @@ def sign_cookie(
127127
url_prefix: URL prefix to sign.
128128
key_name: name of the signing key.
129129
base64_key: signing key as a base64 encoded string.
130-
expiration_time: expiration time.
130+
expiration_time: expiration time as time-zone aware datetime.
131131
132132
Returns:
133133
Returns the Cloud-CDN-Cookie value based on the specified configuration.
134134
"""
135135
encoded_url_prefix = base64.urlsafe_b64encode(
136136
url_prefix.strip().encode("utf-8")
137137
).decode("utf-8")
138-
epoch = datetime.utcfromtimestamp(0)
138+
epoch = datetime.fromtimestamp(0, timezone.utc)
139139
expiration_timestamp = int((expiration_time - epoch).total_seconds())
140140
decoded_key = base64.urlsafe_b64decode(base64_key)
141141

@@ -167,7 +167,7 @@ def sign_cookie(
167167
sign_url_parser.add_argument("base64_key", help="The base64 encoded signing key.")
168168
sign_url_parser.add_argument(
169169
"expiration_time",
170-
type=lambda d: datetime.utcfromtimestamp(float(d)),
170+
type=lambda d: datetime.fromtimestamp(float(d), timezone.utc),
171171
help="Expiration time expessed as seconds since the epoch.",
172172
)
173173

@@ -185,7 +185,7 @@ def sign_cookie(
185185
)
186186
sign_url_prefix_parser.add_argument(
187187
"expiration_time",
188-
type=lambda d: datetime.utcfromtimestamp(float(d)),
188+
type=lambda d: datetime.fromtimestamp(float(d), timezone.utc),
189189
help="Expiration time expessed as seconds since the epoch.",
190190
)
191191

@@ -200,7 +200,7 @@ def sign_cookie(
200200
)
201201
sign_cookie_parser.add_argument(
202202
"expiration_time",
203-
type=lambda d: datetime.utcfromtimestamp(float(d)),
203+
type=lambda d: datetime.fromtimestamp(float(d), timezone.utc),
204204
help="Expiration time expressed as seconds since the epoch.",
205205
)
206206

cdn/snippets_test.py

+63-8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import datetime
2020

21+
import pytest
22+
2123
import snippets
2224

2325

@@ -27,7 +29,7 @@ def test_sign_url() -> None:
2729
"http://35.186.234.33/index.html",
2830
"my-key",
2931
"nZtRohdNF9m3cKM24IcK4w==",
30-
datetime.datetime.utcfromtimestamp(1549751401),
32+
datetime.datetime.fromtimestamp(1549751401, datetime.timezone.utc),
3133
)
3234
== "http://35.186.234.33/index.html?Expires=1549751401&KeyName=my-key&Signature=CRFqQnVfFyiUyR63OQf-HRUpIwc="
3335
)
@@ -37,7 +39,7 @@ def test_sign_url() -> None:
3739
"http://www.example.com/",
3840
"my-key",
3941
"nZtRohdNF9m3cKM24IcK4w==",
40-
datetime.datetime.utcfromtimestamp(1549751401),
42+
datetime.datetime.fromtimestamp(1549751401, datetime.timezone.utc),
4143
)
4244
== "http://www.example.com/?Expires=1549751401&KeyName=my-key&Signature=OqDUFfHpN5Vxga6r80bhsgxKves="
4345
)
@@ -46,19 +48,36 @@ def test_sign_url() -> None:
4648
"http://www.example.com/some/path?some=query&another=param",
4749
"my-key",
4850
"nZtRohdNF9m3cKM24IcK4w==",
49-
datetime.datetime.utcfromtimestamp(1549751401),
51+
datetime.datetime.fromtimestamp(1549751401, datetime.timezone.utc),
5052
)
5153
== "http://www.example.com/some/path?some=query&another=param&Expires=1549751401&KeyName=my-key&Signature=9Q9TCxSju8-W5nUkk5CuTrun2_o="
5254
)
5355

5456

57+
def test_sign_url_raise_exception_on_naive_expiration_datetime() -> None:
58+
with pytest.raises(TypeError):
59+
snippets.sign_url(
60+
"http://www.example.com/some/path?some=query&another=param",
61+
"my-key",
62+
"nZtRohdNF9m3cKM24IcK4w==",
63+
datetime.datetime.fromtimestamp(1549751401),
64+
)
65+
with pytest.raises(TypeError):
66+
snippets.sign_url(
67+
"http://www.example.com/some/path?some=query&another=param",
68+
"my-key",
69+
"nZtRohdNF9m3cKM24IcK4w==",
70+
datetime.datetime.utcfromtimestamp(1549751401),
71+
)
72+
73+
5574
def test_sign_url_prefix() -> None:
5675
assert snippets.sign_url_prefix(
5776
"http://35.186.234.33/index.html",
5877
"http://35.186.234.33/",
5978
"my-key",
6079
"nZtRohdNF9m3cKM24IcK4w==",
61-
datetime.datetime.utcfromtimestamp(1549751401),
80+
datetime.datetime.fromtimestamp(1549751401, datetime.timezone.utc),
6281
) == (
6382
"http://35.186.234.33/index.html?URLPrefix=aHR0cDovLzM1LjE4Ni4yMzQuMzMv&"
6483
"Expires=1549751401&KeyName=my-key&Signature=j7HYgoQ8dIOVsW3Rw4cpkjWfRMA="
@@ -68,7 +87,7 @@ def test_sign_url_prefix() -> None:
6887
"http://www.example.com/",
6988
"my-key",
7089
"nZtRohdNF9m3cKM24IcK4w==",
71-
datetime.datetime.utcfromtimestamp(1549751401),
90+
datetime.datetime.fromtimestamp(1549751401, datetime.timezone.utc),
7291
) == (
7392
"http://www.example.com/?URLPrefix=aHR0cDovL3d3dy5leGFtcGxlLmNvbS8=&"
7493
"Expires=1549751401&KeyName=my-key&Signature=UdT5nVks6Hh8QFMJI9kmXuXYBk0="
@@ -78,21 +97,40 @@ def test_sign_url_prefix() -> None:
7897
"http://www.example.com/some/",
7998
"my-key",
8099
"nZtRohdNF9m3cKM24IcK4w==",
81-
datetime.datetime.utcfromtimestamp(1549751401),
100+
datetime.datetime.fromtimestamp(1549751401, datetime.timezone.utc),
82101
) == (
83102
"http://www.example.com/some/path?some=query&another=param&"
84103
"URLPrefix=aHR0cDovL3d3dy5leGFtcGxlLmNvbS9zb21lLw==&"
85104
"Expires=1549751401&KeyName=my-key&Signature=3th4ThmpS95I1TAKYyYSCSq3dnQ="
86105
)
87106

88107

108+
def test_sign_url_prefix_raise_exception_on_naive_expiration_datetime() -> None:
109+
with pytest.raises(TypeError):
110+
snippets.sign_url_prefix(
111+
"http://www.example.com/some/path?some=query&another=param",
112+
"http://www.example.com/some/",
113+
"my-key",
114+
"nZtRohdNF9m3cKM24IcK4w==",
115+
datetime.datetime.fromtimestamp(1549751401),
116+
)
117+
with pytest.raises(TypeError):
118+
snippets.sign_url_prefix(
119+
"http://www.example.com/some/path?some=query&another=param",
120+
"http://www.example.com/some/",
121+
"my-key",
122+
"nZtRohdNF9m3cKM24IcK4w==",
123+
datetime.datetime.utcfromtimestamp(1549751401),
124+
)
125+
126+
89127
def test_sign_cookie() -> None:
90128
assert (
91129
snippets.sign_cookie(
92130
"http://35.186.234.33/index.html",
93131
"my-key",
94132
"nZtRohdNF9m3cKM24IcK4w==",
95-
datetime.datetime.utcfromtimestamp(1549751401),
133+
datetime.datetime.fromtimestamp(1549751401, datetime.timezone.utc),
96134
)
97135
== "Cloud-CDN-Cookie=URLPrefix=aHR0cDovLzM1LjE4Ni4yMzQuMzMvaW5kZXguaHRtbA==:Expires=1549751401:KeyName=my-key:Signature=uImwlOBCPs91mlCyG9vyyZRrNWU="
98136
)
@@ -102,7 +140,24 @@ def test_sign_cookie() -> None:
102140
"http://www.example.com/foo/",
103141
"my-key",
104142
"nZtRohdNF9m3cKM24IcK4w==",
105-
datetime.datetime.utcfromtimestamp(1549751401),
143+
datetime.datetime.fromtimestamp(1549751401, datetime.timezone.utc),
106144
)
107145
== "Cloud-CDN-Cookie=URLPrefix=aHR0cDovL3d3dy5leGFtcGxlLmNvbS9mb28v:Expires=1549751401:KeyName=my-key:Signature=Z9uYAu73YHioRScZDxnP-TnS274="
108146
)
147+
148+
149+
def test_sign_cookie_raise_exception_on_naive_expiration_datetime() -> None:
150+
with pytest.raises(TypeError):
151+
snippets.sign_cookie(
152+
"http://www.example.com/foo/",
153+
"my-key",
154+
"nZtRohdNF9m3cKM24IcK4w==",
155+
datetime.datetime.fromtimestamp(1549751401),
156+
)
157+
with pytest.raises(TypeError):
158+
snippets.sign_cookie(
159+
"http://www.example.com/foo/",
160+
"my-key",
161+
"nZtRohdNF9m3cKM24IcK4w==",
162+
datetime.datetime.utcfromtimestamp(1549751401),
163+
)

0 commit comments

Comments
 (0)