mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-10 16:31:05 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b3daa313a |
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}
|
#### [`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}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in new issue
Block a user