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}
|
||||
|
||||
: 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}
|
||||
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ dependencies = [
|
||||
"gotenberg-client[httpx]~=1.0",
|
||||
"httpx-oauth~=0.17",
|
||||
"ijson>=3.5.1",
|
||||
"imap-tools>=1.16,<1.17",
|
||||
"imap-tools>=1.14,<1.16",
|
||||
"jinja2~=3.1.6",
|
||||
"langdetect~=1.0.9",
|
||||
"llama-index-core>=0.14.23",
|
||||
@@ -62,7 +62,7 @@ dependencies = [
|
||||
"pdf2image~=1.17.0",
|
||||
"python-dateutil~=2.9.0",
|
||||
"python-dotenv~=1.2.1",
|
||||
"python-gnupg~=0.5.7",
|
||||
"python-gnupg~=0.5.4",
|
||||
"python-ipware~=3.0.0",
|
||||
"python-magic~=0.4.27",
|
||||
"rapidfuzz~=3.14.5",
|
||||
|
||||
@@ -450,7 +450,10 @@ CORS_EXPOSE_HEADERS = [
|
||||
"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 != ["*"]:
|
||||
# always allow localhost. Necessary e.g. for healthcheck in docker.
|
||||
ALLOWED_HOSTS.append("localhost")
|
||||
@@ -460,13 +463,15 @@ 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
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest import mock
|
||||
|
||||
@@ -168,8 +172,66 @@ 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"),
|
||||
[
|
||||
(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,
|
||||
url: str,
|
||||
) -> None:
|
||||
@@ -179,9 +241,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()
|
||||
@@ -1958,11 +1958,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "imap-tools"
|
||||
version = "1.16.0"
|
||||
version = "1.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/ff/013a8cc97cc3d7a74a5cc0d05c5eb637d47a5114efe10312ba7b72b11609/imap_tools-1.16.0.tar.gz", hash = "sha256:70cc760915965f9efd3ce2b43cbdafeca40849aedbea3623019536dc1def95b6", size = 49867, upload-time = "2026-10-14T10:17:57.817Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/59/4e/acf9a7d59932548bc749fe0f5ae852e6da00b640303ed72786f697944b69/imap_tools-1.15.0.tar.gz", hash = "sha256:a90f948549ab000d96caf17c90e63b114f8788ba4a0d3f9274a96c0e108c3805", size = 48497, upload-time = "2026-08-06T10:25:26.567Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/92/d8/b9797cfdbfb885c0ebce581234e5e09ec1805f5d8b42247f8d9cde23ca92/imap_tools-1.16.0-py3-none-any.whl", hash = "sha256:8ae0860d626d44ad21c3b917309d135eb4d112159d4ce78b90a03b9bb9ba0de9", size = 36716, upload-time = "2026-10-14T10:17:54.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/eb/a4ad6ec962af2806c3484adb1869c24a04d8ef8c2034f2dd2b0f48459b0a/imap_tools-1.15.0-py3-none-any.whl", hash = "sha256:7bb0003c8bf124b80968c7bef786e4577d8365e9afb6551dc15b13161b688bdc", size = 36512, upload-time = "2026-08-06T10:25:24.377Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3258,7 +3258,7 @@ requires-dist = [
|
||||
{ name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = ">=2.7,<2.9" },
|
||||
{ name = "httpx-oauth", specifier = "~=0.17" },
|
||||
{ name = "ijson", specifier = ">=3.5.1" },
|
||||
{ name = "imap-tools", specifier = ">=1.16,<1.17" },
|
||||
{ name = "imap-tools", specifier = ">=1.14,<1.16" },
|
||||
{ name = "jinja2", specifier = "~=3.1.6" },
|
||||
{ name = "langdetect", specifier = "~=1.0.9" },
|
||||
{ name = "llama-index-core", specifier = ">=0.14.23" },
|
||||
@@ -3281,7 +3281,7 @@ requires-dist = [
|
||||
{ name = "psycopg-pool", marker = "extra == 'postgres'", specifier = "==3.3.3" },
|
||||
{ name = "python-dateutil", specifier = "~=2.9.0" },
|
||||
{ name = "python-dotenv", specifier = "~=1.2.1" },
|
||||
{ name = "python-gnupg", specifier = "~=0.5.7" },
|
||||
{ name = "python-gnupg", specifier = "~=0.5.4" },
|
||||
{ name = "python-ipware", specifier = "~=3.0.0" },
|
||||
{ name = "python-magic", specifier = "~=0.4.27" },
|
||||
{ name = "rapidfuzz", specifier = "~=3.14.5" },
|
||||
@@ -4196,11 +4196,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-gnupg"
|
||||
version = "0.5.7"
|
||||
version = "0.5.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/d4/47aa0f34b6a06a976063e3e0bf1512b140ec1e6efda83bc717fac58071db/python_gnupg-0.5.7.tar.gz", hash = "sha256:73ea46219f992b361eb1ce54cb0968101670654454916a3ee5df8bb9bf0cc8cc", size = 63974, upload-time = "2026-09-29T16:40:09.487Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/2c/6cd2c7cff4bdbb434be5429ef6b8e96ee6b50155551361f30a1bb2ea3c1d/python_gnupg-0.5.6.tar.gz", hash = "sha256:5743e96212d38923fc19083812dc127907e44dbd3bcf0db4d657e291d3c21eac", size = 66825, upload-time = "2025-12-31T17:19:33.19Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/38/ffad2b8a27e7295e70616ab4d938cead165a56db4496558491f485fa3a87/python_gnupg-0.5.7-py2.py3-none-any.whl", hash = "sha256:dc7afba57a9bc50163c27c726c66cb2fc9692248597f5201f4f7d9eb6097dd1d", size = 22362, upload-time = "2026-09-29T16:40:08.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/ab/0ea9de971caf3cd2e268d2b05dfe9883b21cfe686a59249bd2dccb4bae33/python_gnupg-0.5.6-py2.py3-none-any.whl", hash = "sha256:b5050a55663d8ab9fcc8d97556d229af337a87a3ebebd7054cbd8b7e2043394a", size = 22082, upload-time = "2025-12-31T17:16:22.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user