Skip to content

Commit a54d477

Browse files
committed
fix(auth): replace resource URI regex with linear validation
1 parent c726f98 commit a54d477

2 files changed

Lines changed: 45 additions & 2 deletions

File tree

‎aws_lambda_powertools/utilities/auth_alpha/oauth2/client.py‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import time
77
from collections.abc import Mapping
88
from dataclasses import dataclass, field
9+
from string import ascii_letters, digits, hexdigits
910
from typing import TYPE_CHECKING, Any
1011
from urllib.parse import quote_plus, urlencode, urlsplit
1112

@@ -27,7 +28,20 @@
2728

2829
_BEARER_TOKEN = re.compile(r"[-A-Za-z0-9._~+/]+=*")
2930
_HEADER_NAME = re.compile(r"[-!#$%&'*+.^_`|~0-9A-Za-z]+")
30-
_RESOURCE_URI = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:(?:[A-Za-z0-9._~:/?\[\]@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})*")
31+
_RESOURCE_CHARACTERS = frozenset(ascii_letters + digits + "-._~:/?[]@!$&'()*+,;=")
32+
_HEX_DIGITS = frozenset(hexdigits)
33+
34+
35+
def _valid_resource_characters(resource: str) -> bool:
36+
"""Scan once, accepting URI characters and complete percent escapes, but no fragment."""
37+
characters = iter(resource)
38+
for character in characters:
39+
if character == "%":
40+
if next(characters, "") not in _HEX_DIGITS or next(characters, "") not in _HEX_DIGITS:
41+
return False
42+
elif character not in _RESOURCE_CHARACTERS:
43+
return False
44+
return True
3145

3246

3347
@dataclass(frozen=True)
@@ -138,7 +152,9 @@ def __repr__(self) -> str:
138152
@staticmethod
139153
def _validate_resource(resource: str) -> None:
140154
try:
141-
valid = bool(_RESOURCE_URI.fullmatch(resource)) and bool(urlsplit(resource).scheme)
155+
scheme = urlsplit(resource).scheme
156+
# Older urlsplit versions also recognize schemes starting with a digit.
157+
valid = scheme[:1].isalpha() and _valid_resource_characters(resource)
142158
except ValueError:
143159
valid = False
144160
if not valid:

‎tests/functional/auth_alpha/oauth2/test_client.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,14 @@ def test_invalid_configuration_encoding_is_rejected(field):
565565
"https://inventory.example.com/stock item",
566566
"https://inventory.example.com/%invalid",
567567
"https://[invalid",
568+
"1inventory:stock",
569+
"urn%3Ainventory",
570+
"urn:inventory%",
571+
"urn:inventory%2",
572+
"urn:inventory%2G",
573+
"urn:inventory%G2",
574+
"urn:inventory%%20",
575+
"urn:inventorý",
568576
],
569577
)
570578
def test_resource_must_be_an_absolute_uri_without_a_fragment(resource):
@@ -578,6 +586,9 @@ def test_resource_must_be_an_absolute_uri_without_a_fragment(resource):
578586
{"resource": "urn:example:inventory"},
579587
{"resource": "https://inventory.example.com/stock?region=eu&category=%23parts"},
580588
{"resource": "http://inventory.example.com"},
589+
{"resource": "URN:example:inventory"},
590+
{"resource": "inventory+v1.2-test:stock%2fitems%20eu?category=%23"},
591+
{"resource": "https://[::1]/caf%C3%A9?encoded=%00%ff"},
581592
{"audience": "inventory"},
582593
],
583594
)
@@ -594,6 +605,22 @@ def test_valid_resource_identifiers_and_provider_audiences_are_preserved(http, s
594605
assert other not in fields
595606

596607

608+
@pytest.mark.parametrize("suffix", ["%", "%4", "%4G", "#"])
609+
def test_long_resources_with_invalid_suffixes_are_rejected_before_loading_credentials(http, suffix):
610+
secrets = []
611+
612+
def load_secret():
613+
secrets.append("test-secret")
614+
return secrets[-1]
615+
616+
resource = "urn:inventory:" + "a%41" * 25_000 + suffix
617+
with pytest.raises(ValueError, match="absolute URI without a fragment"):
618+
client(resource=resource, client_secret=load_secret)
619+
620+
assert secrets == []
621+
assert http.requests == []
622+
623+
597624
def test_secret_lookup_does_not_expire_a_new_short_lived_token(http, clock):
598625
def load_secret():
599626
clock.advance(2)

0 commit comments

Comments
 (0)