diff --git a/config.py b/config.py index fc82f82..a2b6c33 100644 --- a/config.py +++ b/config.py @@ -211,21 +211,72 @@ CACHE_RESPONSE = True # Trusted URLs for default redirection TRUSTED_DEFAULT_URLS = [ - "https://ui-avatars.com/api/", - "http://gravatar.com/avatar/", - "https://gravatar.com/avatar/", - "http://www.gravatar.org/avatar/", - "https://www.gravatar.org/avatar/", - "https://secure.gravatar.com/avatar/", - "http://0.gravatar.com/avatar/", - "https://0.gravatar.com/avatar/", - "http://www.gravatar.com/avatar/", - "https://www.gravatar.com/avatar/", - "https://avatars.dicebear.com/api/", - "https://badges.fedoraproject.org/static/img/", - "http://www.planet-libre.org/themes/planetlibre/images/", - "https://www.azuracast.com/img/", - "https://reps.mozilla.org/static/base/img/remo/", + { + "schemes": [ + "https" + ], + "host_equals": "ui-avatars.com", + "path_prefix": "/api/" + }, + { + "schemes": [ + "http", + "https" + ], + "host_equals": "gravatar.com", + "path_prefix": "/avatar/" + }, + { + "schemes": [ + "http", + "https" + ], + "host_suffix": ".gravatar.com", + "path_prefix": "/avatar/" + }, + { + "schemes": [ + "http", + "https" + ], + "host_equals": "www.gravatar.org", + "path_prefix": "/avatar/" + }, + { + "schemes": [ + "https" + ], + "host_equals": "avatars.dicebear.com", + "path_prefix": "/api/" + }, + { + "schemes": [ + "https" + ], + "host_equals": "badges.fedoraproject.org", + "path_prefix": "/static/img/" + }, + { + "schemes": [ + "http", + ], + "host_equals": "www.planet-libre.org", + "path_prefix": "/themes/planetlibre/images/" + }, + { + "schemes": [ + "https" + ], + "host_equals": "www.azuracast.com", + "path_prefix": "/img/" + }, + { + "schemes": [ + "https" + ], + "host_equals": "reps.mozilla.org", + "path_prefix": "/static/base/img/remo/" + } ] # This MUST BE THE LAST! diff --git a/ivatar/test_utils.py b/ivatar/test_utils.py index 1436921..909a512 100644 --- a/ivatar/test_utils.py +++ b/ivatar/test_utils.py @@ -5,7 +5,7 @@ Test our utils from ivatar.utils from django.test import TestCase -from ivatar.utils import openid_variations +from ivatar.utils import is_trusted_url, openid_variations class Tester(TestCase): @@ -45,3 +45,60 @@ class Tester(TestCase): self.assertEqual(openid_variations(openid3)[1], openid1) self.assertEqual(openid_variations(openid3)[2], openid2) self.assertEqual(openid_variations(openid3)[3], openid3) + + def test_is_trusted_url(self): + test1 = is_trusted_url("https://gravatar.com/avatar/63a75a80e6b1f4adfdb04c1ca02e596c", [ + { + "schemes": [ + "http", + "https" + ], + "host_equals": "gravatar.com", + "path_prefix": "/avatar/" + } + ]) + self.assertTrue(test1) + + test2 = is_trusted_url("https://gravatar.com.example.org/avatar/63a75a80e6b1f4adfdb04c1ca02e596c", [ + { + "schemes": [ + "http", + "https" + ], + "host_suffix": ".gravatar.com", + "path_prefix": "/avatar/" + } + ]) + self.assertFalse(test2) + + # Test against open redirect with valid URL in query params + test3 = is_trusted_url("https://github.com/SethFalco/?boop=https://secure.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50", [ + { + "schemes": [ + "http", + "https" + ], + "host_suffix": ".gravatar.com", + "path_prefix": "/avatar/" + } + ]) + self.assertFalse(test3) + + test4 = is_trusted_url("https://ui-avatars.com/api/blah", [ + { + "schemes": [ + "https" + ], + "host_equals": "ui-avatars.com", + "path_prefix": "/api/" + }, + { + "schemes": [ + "http", + "https" + ], + "host_suffix": ".gravatar.com", + "path_prefix": "/avatar/" + } + ]) + self.assertTrue(test4) diff --git a/ivatar/utils.py b/ivatar/utils.py index 280dba3..dc91b46 100644 --- a/ivatar/utils.py +++ b/ivatar/utils.py @@ -5,6 +5,7 @@ Simple module providing reusable random_string function import random import string from PIL import Image, ImageDraw +from urllib.parse import urlparse def random_string(length=10): @@ -112,3 +113,42 @@ def mm_ng( ) return image + + +def is_trusted_url(url, url_filters): + """ + Check if a URL is valid and considered a trusted URL. + If the URL is malformed, returns False. + + Based on: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/events/UrlFilter + """ + (scheme, netloc, path, params, query, fragment) = urlparse(url) + + for filter in url_filters: + if "schemes" in filter: + schemes = filter["schemes"] + + if scheme not in schemes: + continue + + if "host_equals" in filter: + host_equals = filter["host_equals"] + + if netloc != host_equals: + continue + + if "host_suffix" in filter: + host_suffix = filter["host_suffix"] + + if not netloc.endswith(host_suffix): + continue + + if "path_prefix" in filter: + path_prefix = filter["path_prefix"] + + if not path.startswith(path_prefix): + continue + + return True + + return False diff --git a/ivatar/views.py b/ivatar/views.py index f0ea0d9..e5e24cd 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -34,7 +34,7 @@ from .ivataraccount.models import ConfirmedEmail, ConfirmedOpenId from .ivataraccount.models import UnconfirmedEmail, UnconfirmedOpenId from .ivataraccount.models import Photo from .ivataraccount.models import pil_format, file_format -from .utils import mm_ng +from .utils import is_trusted_url, mm_ng URL_TIMEOUT = 5 # in seconds @@ -146,7 +146,9 @@ class AvatarImageView(TemplateView): # Check for :// (schema) if default is not None and default.find("://") > 0: # Check if it's trusted, if not, reset to None - if not any(x in default for x in TRUSTED_DEFAULT_URLS): + trusted_url = is_trusted_url(default, TRUSTED_DEFAULT_URLS) + + if not trusted_url: print( "Default URL is not in trusted URLs: '%s' ; Kicking it!" % default )