mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-11 08:41:33 +00:00
Fix: fix allowed_hosts default (#14735)
This commit is contained in:
1 parent
2829e78009
commit
00096e0252
3 files changed
+118
-22
No files matched your search
+11
-11
@@ -501,20 +501,20 @@ See the [Django project documentation on the settings](https://docs.djangoprojec
|
||||
|
||||
#### [`PAPERLESS_ALLOWED_HOSTS=<comma-separated-list>`](#PAPERLESS_ALLOWED_HOSTS) {#PAPERLESS_ALLOWED_HOSTS}
|
||||
|
||||
: If you're planning on putting Paperless on the open internet, then
|
||||
you really should set this value to the domain name you're using.
|
||||
Failing to do so leaves you open to HTTP host header attacks.
|
||||
: If you expose Paperless to the internet, set `PAPERLESS_URL` to its public URL
|
||||
or list its public hostname in `PAPERLESS_ALLOWED_HOSTS` to help protect against
|
||||
HTTP Host header attacks.
|
||||
You can read more about this in [the Django project's documentation](https://docs.djangoproject.com/en/4.1/topics/security/#host-header-validation)
|
||||
|
||||
Just remember that this is a comma-separated list, so
|
||||
"example.com" is fine, as is "example.com,www.example.com", but
|
||||
NOT " example.com" or "example.com,"
|
||||
Separate multiple hostnames with commas, as in `example.com,www.example.com`.
|
||||
|
||||
Can also be set using PAPERLESS_URL (see above).
|
||||
|
||||
"localhost" is always allowed for docker healthcheck
|
||||
|
||||
Defaults to "\*", which is all hosts.
|
||||
- With neither setting configured, Paperless accepts any host.
|
||||
- With only `PAPERLESS_URL` set, Paperless accepts the hostname from that URL
|
||||
and `localhost` (used by the Docker health check).
|
||||
- `PAPERLESS_ALLOWED_HOSTS` adds hosts to `localhost` and the hostname from
|
||||
`PAPERLESS_URL`, if set. A blank list adds no hosts.
|
||||
- With `PAPERLESS_ALLOWED_HOSTS=*`, Paperless accepts any host, even when
|
||||
`PAPERLESS_URL` is set.
|
||||
|
||||
#### [`PAPERLESS_CORS_ALLOWED_HOSTS=<comma-separated-list>`](#PAPERLESS_CORS_ALLOWED_HOSTS) {#PAPERLESS_CORS_ALLOWED_HOSTS}
|
||||
|
||||
|
||||
@@ -450,23 +450,34 @@ CORS_EXPOSE_HEADERS = [
|
||||
"Content-Disposition",
|
||||
]
|
||||
|
||||
ALLOWED_HOSTS = get_list_from_env("PAPERLESS_ALLOWED_HOSTS", default=["*"])
|
||||
if ALLOWED_HOSTS != ["*"]:
|
||||
# always allow localhost. Necessary e.g. for healthcheck in docker.
|
||||
ALLOWED_HOSTS.append("localhost")
|
||||
|
||||
def _get_allowed_hosts() -> list[str]:
|
||||
if "PAPERLESS_ALLOWED_HOSTS" in os.environ or os.getenv("PAPERLESS_URL"):
|
||||
allowed_hosts = get_list_from_env("PAPERLESS_ALLOWED_HOSTS")
|
||||
else:
|
||||
allowed_hosts = ["*"]
|
||||
if allowed_hosts != ["*"]:
|
||||
# Always allow localhost for the Docker health check.
|
||||
allowed_hosts.append("localhost")
|
||||
return allowed_hosts
|
||||
|
||||
|
||||
ALLOWED_HOSTS = _get_allowed_hosts()
|
||||
|
||||
|
||||
def _parse_paperless_url():
|
||||
global CSRF_TRUSTED_ORIGINS, CORS_ALLOWED_ORIGINS, ALLOWED_HOSTS
|
||||
url = os.getenv("PAPERLESS_URL")
|
||||
if url:
|
||||
hostname = urlparse(url).hostname
|
||||
if not hostname:
|
||||
hostname = (urlparse(url).hostname or "").removesuffix(".")
|
||||
if not hostname or hostname == "*" or hostname.startswith("."):
|
||||
raise ImproperlyConfigured(
|
||||
f"PAPERLESS_URL {url!r} has no hostname, expected a URL like https://example.com",
|
||||
f"PAPERLESS_URL {url!r} has no valid hostname, expected a URL like https://example.com",
|
||||
)
|
||||
CSRF_TRUSTED_ORIGINS.append(url)
|
||||
CORS_ALLOWED_ORIGINS.append(url)
|
||||
if ":" in hostname and not hostname.startswith("["):
|
||||
hostname = f"[{hostname}]" # Add brackets around IPv6 addresses
|
||||
ALLOWED_HOSTS.append(hostname)
|
||||
|
||||
return url
|
||||
|
||||
@@ -3,9 +3,14 @@ from unittest import TestCase
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.core.exceptions import DisallowedHost
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpRequest
|
||||
from django.test import override_settings
|
||||
|
||||
import paperless.settings as paperless_settings
|
||||
from paperless.settings import _get_allauth_trusted_proxy_count
|
||||
from paperless.settings import _get_allowed_hosts
|
||||
from paperless.settings import _get_classifier_language_setting
|
||||
from paperless.settings import _get_search_language_setting
|
||||
from paperless.settings import _parse_paperless_url
|
||||
@@ -168,8 +173,88 @@ class TestPaperlessURLSettings(TestCase):
|
||||
self.assertIn(url, settings.CORS_ALLOWED_ORIGINS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", ["example.com", "https://", "/just/a/path"])
|
||||
def test_paperless_url_without_hostname_is_rejected(
|
||||
@pytest.mark.parametrize(
|
||||
("url", "allowed_hosts", "expected", "accepted_host", "rejected_host"),
|
||||
[
|
||||
(None, None, ["*"], "other.example.com", None),
|
||||
(None, "", ["localhost"], "localhost:8000", "other.example.com"),
|
||||
(
|
||||
None,
|
||||
"lan.example.com",
|
||||
["lan.example.com", "localhost"],
|
||||
"lan.example.com",
|
||||
"other.example.com",
|
||||
),
|
||||
(None, "*", ["*"], "other.example.com", None),
|
||||
(
|
||||
"https://example.com:8443",
|
||||
None,
|
||||
["localhost", "example.com"],
|
||||
"example.com:8443",
|
||||
"other.example.com",
|
||||
),
|
||||
(
|
||||
"https://example.com.",
|
||||
"",
|
||||
["localhost", "example.com"],
|
||||
"example.com.",
|
||||
"other.example.com",
|
||||
),
|
||||
(
|
||||
"http://[::1]:8000",
|
||||
None,
|
||||
["localhost", "[::1]"],
|
||||
"[::1]:8000",
|
||||
"other.example.com",
|
||||
),
|
||||
("https://example.com", "*", ["*", "example.com"], "other.example.com", None),
|
||||
(
|
||||
"https://example.com",
|
||||
"lan.example.com",
|
||||
["lan.example.com", "localhost", "example.com"],
|
||||
"lan.example.com",
|
||||
"other.example.com",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_allowed_hosts_at_startup(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
url: str | None,
|
||||
allowed_hosts: str | None,
|
||||
expected: list[str],
|
||||
accepted_host: str,
|
||||
rejected_host: str | None,
|
||||
) -> None:
|
||||
for name, value in (
|
||||
("PAPERLESS_URL", url),
|
||||
("PAPERLESS_ALLOWED_HOSTS", allowed_hosts),
|
||||
):
|
||||
if value is None:
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
else:
|
||||
monkeypatch.setenv(name, value)
|
||||
|
||||
monkeypatch.setattr(paperless_settings, "ALLOWED_HOSTS", _get_allowed_hosts())
|
||||
monkeypatch.setattr(paperless_settings, "CSRF_TRUSTED_ORIGINS", [])
|
||||
monkeypatch.setattr(paperless_settings, "CORS_ALLOWED_ORIGINS", [])
|
||||
assert _parse_paperless_url() == url
|
||||
assert expected == paperless_settings.ALLOWED_HOSTS
|
||||
|
||||
request = HttpRequest()
|
||||
with override_settings(ALLOWED_HOSTS=expected):
|
||||
request.META["HTTP_HOST"] = accepted_host
|
||||
assert request.get_host() == accepted_host
|
||||
if rejected_host:
|
||||
request.META["HTTP_HOST"] = rejected_host
|
||||
with pytest.raises(DisallowedHost):
|
||||
request.get_host()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url",
|
||||
["example.com", "https://", "/just/a/path", "https://*", "https://.example.com"],
|
||||
)
|
||||
def test_paperless_url_without_valid_hostname_is_rejected(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
url: str,
|
||||
) -> None:
|
||||
@@ -179,9 +264,9 @@ def test_paperless_url_without_hostname_is_rejected(
|
||||
WHEN:
|
||||
- The URL is parsed
|
||||
THEN:
|
||||
- A configuration error is raised instead of adding None to ALLOWED_HOSTS
|
||||
- A configuration error is raised for missing or wildcard hostnames
|
||||
"""
|
||||
monkeypatch.setenv("PAPERLESS_URL", url)
|
||||
|
||||
with pytest.raises(ImproperlyConfigured, match="no hostname"):
|
||||
with pytest.raises(ImproperlyConfigured, match="no valid hostname"):
|
||||
_parse_paperless_url()
|
||||
Reference in new issue
Block a user