From f9ce9e20b2390e90119d46b8bb111c7edb1fd59a Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 20 Oct 2025 16:05:37 +1100 Subject: [PATCH] Fixes for SITE_URL validity checks (#10619) * [docker] Allow HTTPS port to be specified for Caddy proxy * Fix naming collision for INVENTREE_WEB_PORT * Push InvenTree version first * Adjust Caddyfile - Change backup server * Fix docstring * Tweak for site URL check: - Ignore port if SITE_LAX_PROTOCOL_CHECK is set - Invert logic for readability * Additional checks for port mismatch * Adjust middleware checks - Allow for less strict checking of CSRF_TRUSTED_ORIGINS * Slight refactor --- contrib/container/Caddyfile | 4 +- contrib/container/docker-compose.yml | 5 +- src/backend/InvenTree/InvenTree/middleware.py | 56 ++++++++++++++----- .../InvenTree/InvenTree/test_middleware.py | 29 +++++++++- tasks.py | 4 +- 5 files changed, 78 insertions(+), 20 deletions(-) diff --git a/contrib/container/Caddyfile b/contrib/container/Caddyfile index 72a96d1779..6c8d0c973a 100644 --- a/contrib/container/Caddyfile +++ b/contrib/container/Caddyfile @@ -30,9 +30,9 @@ } # The default server address is configured in the .env file -# If not specified, the default address is used - http://inventree.localhost +# If not specified, the proxy listens for all http/https traffic # If you need to listen on multiple addresses, or use a different port, you can modify this section directly -{$INVENTREE_SITE_URL:http://inventree.localhost} { +{$INVENTREE_SITE_URL:"http://, https://"} { import log_common inventree encode gzip diff --git a/contrib/container/docker-compose.yml b/contrib/container/docker-compose.yml index ff4f7a4f7c..5e2e061dd3 100644 --- a/contrib/container/docker-compose.yml +++ b/contrib/container/docker-compose.yml @@ -101,6 +101,7 @@ services: restart: unless-stopped # caddy acts as reverse proxy and static file server + # You can adjust the ports that the proxy listens on via the .env file # https://hub.docker.com/_/caddy inventree-proxy: container_name: inventree-proxy @@ -109,8 +110,8 @@ services: depends_on: - inventree-server ports: - - ${INVENTREE_WEB_PORT:-80}:80 - - 443:443 + - ${INVENTREE_HTTP_PORT:-80}:80 + - ${INVENTREE_HTTPS_PORT:-443}:443 env_file: - .env volumes: diff --git a/src/backend/InvenTree/InvenTree/middleware.py b/src/backend/InvenTree/InvenTree/middleware.py index be2c7e37fd..c84ab0c89d 100644 --- a/src/backend/InvenTree/InvenTree/middleware.py +++ b/src/backend/InvenTree/InvenTree/middleware.py @@ -239,13 +239,29 @@ class InvenTreeHostSettingsMiddleware(MiddlewareMixin): accessed_scheme = request._current_scheme_host referer = urlsplit(accessed_scheme) - # Ensure that the settings are set correctly with the current request - matches = ( - (accessed_scheme and not accessed_scheme.startswith(settings.SITE_URL)) - if not settings.SITE_LAX_PROTOCOL_CHECK - else not is_same_domain(referer.netloc, urlsplit(settings.SITE_URL).netloc) + site_url = urlsplit(settings.SITE_URL) + + # Check if the accessed URL matches the SITE_URL setting + site_url_match = ( + ( + # Exact match on domain + is_same_domain(referer.netloc, site_url.netloc) + and referer.scheme == site_url.scheme + ) + or ( + # Lax protocol match, accessed URL starts with SITE_URL + settings.SITE_LAX_PROTOCOL_CHECK + and accessed_scheme.startswith(settings.SITE_URL) + ) + or ( + # Lax protocol match, same domain + settings.SITE_LAX_PROTOCOL_CHECK + and referer.hostname == site_url.hostname + ) ) - if matches: + + if not site_url_match: + # The accessed URL does not match the SITE_URL setting if ( isinstance(settings.CSRF_TRUSTED_ORIGINS, list) and len(settings.CSRF_TRUSTED_ORIGINS) > 1 @@ -263,17 +279,31 @@ class InvenTreeHostSettingsMiddleware(MiddlewareMixin): request, 'config_error.html', {'error_message': msg}, status=500 ) - # Check trusted origins - if not any( - is_same_domain(referer.netloc, host) - for host in [ - urlsplit(origin).netloc.lstrip('*') + trusted_origins_match = ( + # Matching domain found in allowed origins + any( + is_same_domain(referer.netloc, host) + for host in [ + urlsplit(origin).netloc.lstrip('*') + for origin in settings.CSRF_TRUSTED_ORIGINS + ] + ) + ) or ( + # Lax protocol match allowed + settings.SITE_LAX_PROTOCOL_CHECK + and any( + referer.hostname == urlsplit(origin).hostname for origin in settings.CSRF_TRUSTED_ORIGINS - ] - ): + ) + ) + + # Check trusted origins + if not trusted_origins_match: msg = f'INVE-E7: The used path `{accessed_scheme}` is not in the TRUSTED_ORIGINS' logger.error(msg) return render( request, 'config_error.html', {'error_message': msg}, status=500 ) + + # All checks passed return None diff --git a/src/backend/InvenTree/InvenTree/test_middleware.py b/src/backend/InvenTree/InvenTree/test_middleware.py index 00c0d65091..0a0713eb03 100644 --- a/src/backend/InvenTree/InvenTree/test_middleware.py +++ b/src/backend/InvenTree/InvenTree/test_middleware.py @@ -112,6 +112,15 @@ class MiddlewareTests(InvenTreeTestCase): def test_site_lax_protocol(self): """Test that the site URL check is correctly working with/without lax protocol check.""" + # Test that a completely different host fails + with self.settings( + SITE_URL='https://testserver', CSRF_TRUSTED_ORIGINS=['https://testserver'] + ): + response = self.client.get( + reverse('web'), HTTP_HOST='otherhost.example.com' + ) + self.assertContains(response, 'INVE-E7: The visited path', status_code=500) + # Simple setup with proxy with self.settings( SITE_URL='https://testserver', CSRF_TRUSTED_ORIGINS=['https://testserver'] @@ -128,6 +137,24 @@ class MiddlewareTests(InvenTreeTestCase): response = self.client.get(reverse('web')) self.assertContains(response, 'INVE-E7: The visited path', status_code=500) + def test_site_url_port(self): + """URL checks with different ports.""" + with self.settings( + SITE_URL='https://testserver:8000', + CSRF_TRUSTED_ORIGINS=['https://testserver:8000'], + ): + response = self.client.get(reverse('web'), HTTP_HOST='testserver:8008') + self.do_positive_test(response) + + # Try again with strict protocol check + with self.settings( + SITE_URL='https://testserver:8000', + CSRF_TRUSTED_ORIGINS=['https://testserver:8000'], + SITE_LAX_PROTOCOL_CHECK=False, + ): + response = self.client.get(reverse('web'), HTTP_HOST='testserver:8008') + self.assertContains(response, 'INVE-E7: The visited path', status_code=500) + def test_site_url_checks_multi(self): """Test that the site URL check is correctly working in a multi-site setup.""" # multi-site setup with trusted origins @@ -149,7 +176,7 @@ class MiddlewareTests(InvenTreeTestCase): ) self.do_positive_test(response) - # A non-trsuted origin must still fail in multi - origin setup + # A non-trusted origin must still fail in multi - origin setup response = self.client.get( 'https://not-my-testserver.example.com/web/', SERVER_NAME='not-my-testserver.example.com', diff --git a/tasks.py b/tasks.py index 944e1a1b47..a11064b583 100644 --- a/tasks.py +++ b/tasks.py @@ -1556,10 +1556,10 @@ Static {get_static_dir(error=False) or NOT_SPECIFIED} Backup {get_backup_dir(error=False) or NOT_SPECIFIED} Versions: -Python {python_version()} -Django {InvenTreeVersion.inventreeDjangoVersion()} InvenTree {InvenTreeVersion.inventreeVersion()} API {InvenTreeVersion.inventreeApiVersion()} +Python {python_version()} +Django {InvenTreeVersion.inventreeDjangoVersion()} Node {node if node else NA} Yarn {yarn if yarn else NA}