fix: validation for trusted urls

This commit is contained in:
Seth Falco
2022-07-15 16:04:48 +01:00
parent 67ac0ad973
commit 2578e804b6
4 changed files with 168 additions and 18 deletions

View File

@@ -211,21 +211,72 @@ CACHE_RESPONSE = True
# Trusted URLs for default redirection # Trusted URLs for default redirection
TRUSTED_DEFAULT_URLS = [ TRUSTED_DEFAULT_URLS = [
"https://ui-avatars.com/api/", {
"http://gravatar.com/avatar/", "schemes": [
"https://gravatar.com/avatar/", "https"
"http://www.gravatar.org/avatar/", ],
"https://www.gravatar.org/avatar/", "host_equals": "ui-avatars.com",
"https://secure.gravatar.com/avatar/", "path_prefix": "/api/"
"http://0.gravatar.com/avatar/", },
"https://0.gravatar.com/avatar/", {
"http://www.gravatar.com/avatar/", "schemes": [
"https://www.gravatar.com/avatar/", "http",
"https://avatars.dicebear.com/api/", "https"
"https://badges.fedoraproject.org/static/img/", ],
"http://www.planet-libre.org/themes/planetlibre/images/", "host_equals": "gravatar.com",
"https://www.azuracast.com/img/", "path_prefix": "/avatar/"
"https://reps.mozilla.org/static/base/img/remo/", },
{
"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! # This MUST BE THE LAST!

View File

@@ -5,7 +5,7 @@ Test our utils from ivatar.utils
from django.test import TestCase from django.test import TestCase
from ivatar.utils import openid_variations from ivatar.utils import is_trusted_url, openid_variations
class Tester(TestCase): class Tester(TestCase):
@@ -45,3 +45,60 @@ class Tester(TestCase):
self.assertEqual(openid_variations(openid3)[1], openid1) self.assertEqual(openid_variations(openid3)[1], openid1)
self.assertEqual(openid_variations(openid3)[2], openid2) self.assertEqual(openid_variations(openid3)[2], openid2)
self.assertEqual(openid_variations(openid3)[3], openid3) 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)

View File

@@ -5,6 +5,7 @@ Simple module providing reusable random_string function
import random import random
import string import string
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
from urllib.parse import urlparse
def random_string(length=10): def random_string(length=10):
@@ -112,3 +113,42 @@ def mm_ng(
) )
return image 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

View File

@@ -34,7 +34,7 @@ from .ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
from .ivataraccount.models import UnconfirmedEmail, UnconfirmedOpenId from .ivataraccount.models import UnconfirmedEmail, UnconfirmedOpenId
from .ivataraccount.models import Photo from .ivataraccount.models import Photo
from .ivataraccount.models import pil_format, file_format 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 URL_TIMEOUT = 5 # in seconds
@@ -146,7 +146,9 @@ class AvatarImageView(TemplateView):
# Check for :// (schema) # Check for :// (schema)
if default is not None and default.find("://") > 0: if default is not None and default.find("://") > 0:
# Check if it's trusted, if not, reset to None # 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( print(
"Default URL is not in trusted URLs: '%s' ; Kicking it!" % default "Default URL is not in trusted URLs: '%s' ; Kicking it!" % default
) )