Compare commits

...
Author SHA1 Message Date
shamoon 7b3daa313a Fix: fix allowed_hosts default 2026-11-10 07:53:53 -08:00
3 changed files with 86 additions and 19 deletions

No files matched your search

+11 -11
View File
@@ -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} #### [`PAPERLESS_ALLOWED_HOSTS=<comma-separated-list>`](#PAPERLESS_ALLOWED_HOSTS) {#PAPERLESS_ALLOWED_HOSTS}
: If you're planning on putting Paperless on the open internet, then : If you expose Paperless to the internet, set `PAPERLESS_URL` to its public URL
you really should set this value to the domain name you're using. or list its public hostname in `PAPERLESS_ALLOWED_HOSTS` to help protect against
Failing to do so leaves you open to HTTP host header attacks. 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) 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 Separate multiple hostnames with commas, as in `example.com,www.example.com`.
"example.com" is fine, as is "example.com,www.example.com", but
NOT " example.com" or "example.com,"
Can also be set using PAPERLESS_URL (see above). - With neither setting configured, Paperless accepts any host.
- With only `PAPERLESS_URL` set, Paperless accepts the hostname from that URL
"localhost" is always allowed for docker healthcheck and `localhost` (used by the Docker health check).
- `PAPERLESS_ALLOWED_HOSTS` adds hosts to `localhost` and the hostname from
Defaults to "\*", which is all hosts. `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} #### [`PAPERLESS_CORS_ALLOWED_HOSTS=<comma-separated-list>`](#PAPERLESS_CORS_ALLOWED_HOSTS) {#PAPERLESS_CORS_ALLOWED_HOSTS}
+9 -4
View File
@@ -450,7 +450,10 @@ CORS_EXPOSE_HEADERS = [
"Content-Disposition", "Content-Disposition",
] ]
ALLOWED_HOSTS = get_list_from_env("PAPERLESS_ALLOWED_HOSTS", default=["*"]) 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 != ["*"]: if ALLOWED_HOSTS != ["*"]:
# always allow localhost. Necessary e.g. for healthcheck in docker. # always allow localhost. Necessary e.g. for healthcheck in docker.
ALLOWED_HOSTS.append("localhost") ALLOWED_HOSTS.append("localhost")
@@ -460,13 +463,15 @@ def _parse_paperless_url():
global CSRF_TRUSTED_ORIGINS, CORS_ALLOWED_ORIGINS, ALLOWED_HOSTS global CSRF_TRUSTED_ORIGINS, CORS_ALLOWED_ORIGINS, ALLOWED_HOSTS
url = os.getenv("PAPERLESS_URL") url = os.getenv("PAPERLESS_URL")
if url: if url:
hostname = urlparse(url).hostname hostname = (urlparse(url).hostname or "").removesuffix(".")
if not hostname: if not hostname or hostname == "*" or hostname.startswith("."):
raise ImproperlyConfigured( 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) CSRF_TRUSTED_ORIGINS.append(url)
CORS_ALLOWED_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) ALLOWED_HOSTS.append(hostname)
return url return url
+66 -4
View File
@@ -1,4 +1,8 @@
import json
import os import os
import subprocess
import sys
from pathlib import Path
from unittest import TestCase from unittest import TestCase
from unittest import mock from unittest import mock
@@ -168,8 +172,66 @@ class TestPaperlessURLSettings(TestCase):
self.assertIn(url, settings.CORS_ALLOWED_ORIGINS) self.assertIn(url, settings.CORS_ALLOWED_ORIGINS)
@pytest.mark.parametrize("url", ["example.com", "https://", "/just/a/path"]) @pytest.mark.parametrize(
def test_paperless_url_without_hostname_is_rejected( ("url", "allowed_hosts", "expected"),
[
(None, None, ["*"]),
(None, "", ["localhost"]),
("https://example.com:8443", None, ["localhost", "example.com"]),
("https://example.com.", "", ["localhost", "example.com"]),
("http://[::1]:8000", None, ["localhost", "[::1]"]),
("https://example.com", "*", ["*", "example.com"]),
(
"https://example.com",
"lan.example.com",
["lan.example.com", "localhost", "example.com"],
),
],
)
def test_allowed_hosts_at_startup(
tmp_path: Path,
url: str | None,
allowed_hosts: str | None,
expected: list[str],
) -> None:
config = tmp_path / "paperless.conf"
config.touch()
env = {
**os.environ,
"PAPERLESS_CONFIGURATION_PATH": str(config),
"PAPERLESS_LOGGING_DIR": str(tmp_path),
}
for name, value in (
("PAPERLESS_URL", url),
("PAPERLESS_ALLOWED_HOSTS", allowed_hosts),
):
if value is None:
env.pop(name, None)
else:
env[name] = value
result = subprocess.run(
[
sys.executable,
"-c",
"import json; from paperless.settings import ALLOWED_HOSTS; print(json.dumps(ALLOWED_HOSTS))",
],
cwd=Path(__file__).resolve().parents[3],
env=env,
capture_output=True,
text=True,
timeout=30,
check=False,
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == expected
@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, monkeypatch: pytest.MonkeyPatch,
url: str, url: str,
) -> None: ) -> None:
@@ -179,9 +241,9 @@ def test_paperless_url_without_hostname_is_rejected(
WHEN: WHEN:
- The URL is parsed - The URL is parsed
THEN: 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) monkeypatch.setenv("PAPERLESS_URL", url)
with pytest.raises(ImproperlyConfigured, match="no hostname"): with pytest.raises(ImproperlyConfigured, match="no valid hostname"):
_parse_paperless_url() _parse_paperless_url()