Skip to content

Commit 4015e4d

Browse files
akanstantsinaudopry
authored andcommitted
Added ALLOWED_SCHEMES setting for Allowed Orgins validation
1 parent 7092863 commit 4015e4d

File tree

5 files changed

+114
-4
lines changed

5 files changed

+114
-4
lines changed

docs/settings.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ assigned ports.
6363
Note that you may override ``Application.get_allowed_schemes()`` to set this on
6464
a per-application basis.
6565

66+
ALLOWED_SCHEMES
67+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
68+
69+
Default: ``["https"]``
70+
71+
A list of schemes that the ``allowed_origins`` field will be validated against.
72+
Setting this to ``["https"]`` only in production is strongly recommended.
73+
Adding ``"http"`` to the list is considered to be safe only for local development and testing.
74+
Note that `OAUTHLIB_INSECURE_TRANSPORT <https://oauthlib.readthedocs.io/en/latest/oauth2/security.html#envvar-OAUTHLIB_INSECURE_TRANSPORT>`_
75+
environment variable should be also set to allow http origins.
76+
6677

6778
APPLICATION_MODEL
6879
~~~~~~~~~~~~~~~~~

oauth2_provider/models.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
from .scopes import get_scopes_backend
2121
from .settings import oauth2_settings
2222
from .utils import jwk_from_pem
23-
from .validators import RedirectURIValidator, URIValidator, WildcardSet
24-
23+
from .validators import RedirectURIValidator, URIValidator, WildcardSet, AllowedURIValidator
2524

2625
logger = logging.getLogger(__name__)
2726

@@ -218,7 +217,7 @@ def clean(self):
218217
allowed_origins = self.allowed_origins.strip().split()
219218
if allowed_origins:
220219
# oauthlib allows only https scheme for CORS
221-
validator = URIValidator({"https"})
220+
validator = AllowedURIValidator(oauth2_settings.ALLOWED_SCHEMES, "Origin")
222221
for uri in allowed_origins:
223222
validator(uri)
224223

@@ -808,6 +807,10 @@ def is_origin_allowed(origin, allowed_origins):
808807
"""
809808

810809
parsed_origin = urlparse(origin)
810+
811+
if parsed_origin.scheme not in oauth2_settings.ALLOWED_SCHEMES:
812+
return False
813+
811814
for allowed_origin in allowed_origins:
812815
parsed_allowed_origin = urlparse(allowed_origin)
813816
if (

oauth2_provider/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"REFRESH_TOKEN_ADMIN_CLASS": "oauth2_provider.admin.RefreshTokenAdmin",
6969
"REQUEST_APPROVAL_PROMPT": "force",
7070
"ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https"],
71+
"ALLOWED_SCHEMES": ["https"],
7172
"OIDC_ENABLED": False,
7273
"OIDC_ISS_ENDPOINT": "",
7374
"OIDC_USERINFO_ENDPOINT": "",

oauth2_provider/validators.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,32 @@ def __call__(self, value):
3131
raise ValidationError("Redirect URIs must not contain fragments")
3232

3333

34+
class AllowedURIValidator(URIValidator):
35+
def __init__(self, schemes, name, allow_path=False, allow_query=False, allow_fragments=False):
36+
"""
37+
:params schemes: List of allowed schemes. E.g.: ["https"]
38+
:params name: Name of the validater URI required for validation message. E.g.: "Origin"
39+
:params allow_path: If URI can contain path part
40+
:params allow_query: If URI can contain query part
41+
:params allow_fragments: If URI can contain fragments part
42+
"""
43+
super().__init__(schemes=schemes)
44+
self.name = name
45+
self.allow_path = allow_path
46+
self.allow_query = allow_query
47+
self.allow_fragments = allow_fragments
48+
49+
def __call__(self, value):
50+
super().__call__(value)
51+
value = force_str(value)
52+
scheme, netloc, path, query, fragment = urlsplit(value)
53+
if path and not self.allow_path:
54+
raise ValidationError("{} URIs must not contain path".format(self.name))
55+
if query and not self.allow_query:
56+
raise ValidationError("{} URIs must not contain query".format(self.name))
57+
if fragment and not self.allow_fragments:
58+
raise ValidationError("{} URIs must not contain fragments".format(self.name))
59+
3460
##
3561
# WildcardSet is a special set that contains everything.
3662
# This is required in order to move validation of the scheme from

tests/test_validators.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.core.validators import ValidationError
33
from django.test import TestCase
44

5-
from oauth2_provider.validators import RedirectURIValidator
5+
from oauth2_provider.validators import RedirectURIValidator, AllowedURIValidator
66

77

88
@pytest.mark.usefixtures("oauth2_settings")
@@ -36,6 +36,11 @@ def test_validate_custom_uri_scheme(self):
3636
# Check ValidationError not thrown
3737
validator(uri)
3838

39+
validator = AllowedURIValidator(["my-scheme", "https", "git+ssh"], "Origin")
40+
for uri in good_uris:
41+
# Check ValidationError not thrown
42+
validator(uri)
43+
3944
def test_validate_bad_uris(self):
4045
validator = RedirectURIValidator(allowed_schemes=["https"])
4146
self.oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ["https", "good"]
@@ -61,3 +66,67 @@ def test_validate_bad_uris(self):
6166
for uri in bad_uris:
6267
with self.assertRaises(ValidationError):
6368
validator(uri)
69+
70+
def test_validate_good_origin_uris(self):
71+
"""
72+
Test AllowedURIValidator validates origin URIs if they match requirements
73+
"""
74+
validator = AllowedURIValidator(
75+
["https"],
76+
"Origin",
77+
allow_path=False,
78+
allow_query=False,
79+
allow_fragments=False,
80+
)
81+
good_uris = [
82+
"https://example.com",
83+
"https://example.com:8080",
84+
"https://example",
85+
"https://localhost",
86+
"https://1.1.1.1",
87+
"https://127.0.0.1",
88+
"https://255.255.255.255",
89+
]
90+
for uri in good_uris:
91+
# Check ValidationError not thrown
92+
validator(uri)
93+
94+
def test_validate_bad_origin_uris(self):
95+
"""
96+
Test AllowedURIValidator rejects origin URIs if they do not match requirements
97+
"""
98+
validator = AllowedURIValidator(
99+
["https"],
100+
"Origin",
101+
allow_path=False,
102+
allow_query=False,
103+
allow_fragments=False,
104+
)
105+
bad_uris = [
106+
"http:/example.com",
107+
"HTTP://localhost",
108+
"HTTP://example.com",
109+
"HTTP://example.com.",
110+
"http://example.com/#fragment",
111+
"123://example.com",
112+
"http://fe80::1",
113+
"git+ssh://example.com",
114+
"my-scheme://example.com",
115+
"uri-without-a-scheme",
116+
"https://example.com/#fragment",
117+
"good://example.com/#fragment",
118+
" ",
119+
"",
120+
# Bad IPv6 URL, urlparse behaves differently for these
121+
'https://["><script>alert()</script>',
122+
# Origin uri should not contain path, query of fragment parts
123+
# https://www.rfc-editor.org/rfc/rfc6454#section-7.1
124+
"https:/example.com/",
125+
"https:/example.com/test",
126+
"https:/example.com/?q=test",
127+
"https:/example.com/#test",
128+
]
129+
130+
for uri in bad_uris:
131+
with self.assertRaises(ValidationError):
132+
validator(uri)

0 commit comments

Comments
 (0)