Repository: thumbor/thumbor
Issue: allowed_sources being ignored

Having issues with allowed_domains ignoring our configuration and returning images from any unauthorised url.

### Expected behaviour
https://our.thumborserver.org/t/1925x800/smart/static.externaldomain.com/img/arc_25699_g.jpg
(not real url)
should not be returning an image

### Actual behaviour
any domain seems accepted by thumbor even if not included in allowed_sources

ALLOWED_SOURCES = ['.+.domain1.com', '.+.domain2.net', '.+.domain3.es']

### Operating system
thumbor vo 6.5 on LXC debian container behind a nginx reverse proxy

### Your thumbor.conf
Only including non-default public values:
```
MAX_WIDTH = 2400
MAX_HEIGHT = 3000
MAX_PIXELS = 35000000.0

#ALLOWED_SOURCES = #    [
#    ]
ALLOWED_SOURCES = ['.+.domain1.com', '.+.domain2.net', '.+.domain3.es']
QUALITY = 75

AUTO_WEBP = True

RESULT_STORAGE = 'thumbor.result_storages.file_storage'

LOADER = 'thumbor.loaders.file_loader_http_fallback'

FILE_LOADER_ROOT_PATH = '/home/webs-static/thumbor'


STORAGE_EXPIRATION_SECONDS = 300

FILE_STORAGE_ROOT_PATH = '/cache/thumbor'


DETECTORS = ["thumbor.detectors.feature_detector"]

OPTIMIZERS = [
  'thumbor.optimizers.jpegtran'
]

## Path for the jpegtran binary
## Defaults to: '/usr/bin/jpegtran'
JPEGTRAN_PATH = '/usr/bin/jpegtran'

FILTERS =     [
        'thumbor.filters.autojpg',
        'thumbor.filters.brightness',
        'thumbor.filters.colorize',
        'thumbor.filters.contrast',
        'thumbor.filters.rgb',
        'thumbor.filters.round_corner',
        'thumbor.filters.quality',
        'thumbor.filters.noise',
        'thumbor.filters.watermark',
        'thumbor.filters.equalize',
        'thumbor.filters.fill',
        'thumbor.filters.sharpen',
        'thumbor.filters.strip_exif',
        'thumbor.filters.strip_icc',
        'thumbor.filters.frame',
        'thumbor.filters.grayscale',
        'thumbor.filters.rotate',
        'thumbor.filters.format',
        'thumbor.filters.max_bytes',
        'thumbor.filters.convolution',
        'thumbor.filters.blur',
        'thumbor.filters.extract_focal',
        'thumbor.filters.focal',
        'thumbor.filters.no_upscale',
        'thumbor.filters.saturation',
        'thumbor.filters.max_age',
        'thumbor.filters.curve',
        'thumbor.filters.background_color',
        'thumbor.filters.upscale',
        'thumbor.filters.proportion',
        'thumbor.filters.stretch',
    ]

RESULT_STORAGE_EXPIRATION_SECONDS = 7200
RESULT_STORAGE_FILE_STORAGE_ROOT_PATH = '/cache/thumbor/results'

RESULT_STORAGE_STORES_UNSAFE = True

SENTRY_DSN_URL = 'http://xxxxxxxxxxxxxxxxxx@mon.domain.es:9000/6'

COMMUNITY_EXTENSIONS = [
    'tc_purger'
]
```


Changed files:
tests/loaders/test_file_loader_http_fallback.py, thumbor/loaders/file_loader_http_fallback.py, thumbor/loaders/__init__.py

diff --git a/tests/loaders/test_file_loader_http_fallback.py b/tests/loaders/test_file_loader_http_fallback.py
index b7ee528..820f9a0 100644
--- a/tests/loaders/test_file_loader_http_fallback.py
+++ b/tests/loaders/test_file_loader_http_fallback.py
@@ -31,6 +31,7 @@ async def dummy_file_load(
         successful=True,
         buffer="file",
     )
+
     return result
 
 
@@ -41,6 +42,7 @@ async def dummy_http_load(
         successful=True,
         buffer="http",
     )
+
     return result
 
 
@@ -70,3 +72,18 @@ class FileLoaderHttpFallbackHttpTestCase(TestCase):
 
         expect(result).to_be_instance_of(LoaderResult)
         expect(result.buffer).to_equal("http")
+
+    @patch.object(thumbor.loaders.http_loader, "load", dummy_http_load)
+    @gen_test
+    async def test_should_fail_with_disallowed_origin(self):
+        url = "http:/www.google.com/example_image.png"
+        config = Config(ALLOWED_SOURCES=[".+.domain1.com"])
+        ctx = Context(None, config, None)
+
+        result = await loader.load(ctx, url)
+
+        expect(result).to_be_instance_of(LoaderResult)
+        expect(result.successful).to_be_false()
+        expect(result.error).to_equal(LoaderResult.ERROR_BAD_REQUEST)
+        expect(result.extras["reason"]).to_equal("Unallowed domain")
+        expect(result.extras["source"]).to_equal(url)
diff --git a/thumbor/loaders/__init__.py b/thumbor/loaders/__init__.py
index d2eed95..b4f1fcc 100644
--- a/thumbor/loaders/__init__.py
+++ b/thumbor/loaders/__init__.py
@@ -8,14 +8,24 @@
 # http://www.opensource.org/licenses/mit-license
 # Copyright (c) 2011 globo.com thumbor@googlegroups.com
 
+from typing import Dict
+
 
 class LoaderResult:
 
     ERROR_NOT_FOUND = "not_found"
     ERROR_UPSTREAM = "upstream"
     ERROR_TIMEOUT = "timeout"
-
-    def __init__(self, buffer=None, successful=True, error=None, metadata=None):
+    ERROR_BAD_REQUEST = "bad_request"
+
+    def __init__(
+        self,
+        buffer: bytes = None,
+        successful: bool = True,
+        error: str = None,
+        metadata: Dict[str, any] = None,
+        extras: Dict[str, any] = None,
+    ):
         """
         :param buffer: The media buffer
 
@@ -27,12 +37,19 @@ class LoaderResult:
 
         :param metadata: Dictionary of metadata about the buffer
         :type metadata: dict
+
+        :param extras: Dictionary of extra information about the error
+        :type metadata: dict
         """
 
         if metadata is None:
             metadata = {}
 
+        if extras is None:
+            extras = {}
+
         self.buffer = buffer
         self.successful = successful
         self.error = error
         self.metadata = metadata
+        self.extras = extras
diff --git a/thumbor/loaders/file_loader_http_fallback.py b/thumbor/loaders/file_loader_http_fallback.py
index 816e133..f18329e 100644
--- a/thumbor/loaders/file_loader_http_fallback.py
+++ b/thumbor/loaders/file_loader_http_fallback.py
@@ -8,14 +8,25 @@
 # http://www.opensource.org/licenses/mit-license
 # Copyright (c) 2011 globo.com thumbor@googlegroups.com
 
-from thumbor.loaders import file_loader, http_loader
+from thumbor.loaders import LoaderResult, file_loader, http_loader
 
 
 async def load(context, path):
     # First attempt to load with file_loader
     result = await file_loader.load(context, path)
+
     if result.successful:
         return result
 
     # If file_loader failed try http_loader
+
+    if not http_loader.validate(context, path):
+        result = LoaderResult()
+        result.successful = False
+        result.error = LoaderResult.ERROR_BAD_REQUEST
+        result.extras["reason"] = "Unallowed domain"
+        result.extras["source"] = path
+
+        return result
+
     return await http_loader.load(context, path)