mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-04-02 22:28:51 +00:00
Compare commits
1 Commits
fix-signed
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8b3f2cb3c |
@@ -31,11 +31,6 @@ from paperless.models import ApplicationConfiguration
|
||||
|
||||
|
||||
class TestViews(DirectoriesMixin, TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls) -> None:
|
||||
super().setUpTestData()
|
||||
ApplicationConfiguration.objects.get_or_create()
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create_user("testuser")
|
||||
super().setUp()
|
||||
|
||||
@@ -1,59 +1,11 @@
|
||||
import hmac
|
||||
import os
|
||||
import pickle
|
||||
from hashlib import sha256
|
||||
|
||||
from celery import Celery
|
||||
from celery.signals import worker_process_init
|
||||
from kombu.serialization import register
|
||||
|
||||
# Set the default Django settings module for the 'celery' program.
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless.settings")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signed-pickle serializer: pickle with HMAC-SHA256 integrity verification.
|
||||
#
|
||||
# Protects against malicious pickle injection via an exposed Redis broker.
|
||||
# Messages are signed on the producer side and verified before deserialization
|
||||
# on the worker side using Django's SECRET_KEY.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
HMAC_SIZE = 32 # SHA-256 digest length
|
||||
|
||||
|
||||
def _get_signing_key() -> bytes:
|
||||
from django.conf import settings
|
||||
|
||||
return settings.SECRET_KEY.encode()
|
||||
|
||||
|
||||
def signed_pickle_dumps(obj: object) -> bytes:
|
||||
data = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
signature = hmac.new(_get_signing_key(), data, sha256).digest()
|
||||
return signature + data
|
||||
|
||||
|
||||
def signed_pickle_loads(payload: bytes) -> object:
|
||||
if len(payload) < HMAC_SIZE:
|
||||
msg = "Signed-pickle payload too short"
|
||||
raise ValueError(msg)
|
||||
signature = payload[:HMAC_SIZE]
|
||||
data = payload[HMAC_SIZE:]
|
||||
expected = hmac.new(_get_signing_key(), data, sha256).digest()
|
||||
if not hmac.compare_digest(signature, expected):
|
||||
msg = "Signed-pickle HMAC verification failed — message may have been tampered with"
|
||||
raise ValueError(msg)
|
||||
return pickle.loads(data)
|
||||
|
||||
|
||||
register(
|
||||
"signed-pickle",
|
||||
signed_pickle_dumps,
|
||||
signed_pickle_loads,
|
||||
content_type="application/x-signed-pickle",
|
||||
content_encoding="binary",
|
||||
)
|
||||
|
||||
app = Celery("paperless")
|
||||
|
||||
# Using a string here means the worker doesn't have to serialize
|
||||
|
||||
@@ -667,11 +667,9 @@ CELERY_RESULT_BACKEND = "django-db"
|
||||
CELERY_CACHE_BACKEND = "default"
|
||||
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-serializer
|
||||
# Uses HMAC-signed pickle to prevent RCE via malicious messages on an exposed Redis broker.
|
||||
# The signed-pickle serializer is registered in paperless/celery.py.
|
||||
CELERY_TASK_SERIALIZER = "signed-pickle"
|
||||
CELERY_TASK_SERIALIZER = "pickle"
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std-setting-accept_content
|
||||
CELERY_ACCEPT_CONTENT = ["application/json", "application/x-signed-pickle"]
|
||||
CELERY_ACCEPT_CONTENT = ["application/json", "application/x-python-serialize"]
|
||||
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#beat-schedule
|
||||
CELERY_BEAT_SCHEDULE = parse_beat_schedule()
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import hmac
|
||||
import pickle
|
||||
from hashlib import sha256
|
||||
|
||||
import pytest
|
||||
from django.test import override_settings
|
||||
|
||||
from paperless.celery import HMAC_SIZE
|
||||
from paperless.celery import signed_pickle_dumps
|
||||
from paperless.celery import signed_pickle_loads
|
||||
|
||||
|
||||
class TestSignedPickleSerializer:
|
||||
def test_roundtrip_simple_types(self):
|
||||
"""Signed pickle can round-trip basic JSON-like types."""
|
||||
for obj in [42, "hello", [1, 2, 3], {"key": "value"}, None, True]:
|
||||
assert signed_pickle_loads(signed_pickle_dumps(obj)) == obj
|
||||
|
||||
def test_roundtrip_complex_types(self):
|
||||
"""Signed pickle can round-trip types that JSON cannot."""
|
||||
from pathlib import Path
|
||||
|
||||
obj = {"path": Path("/tmp/test"), "data": {1, 2, 3}}
|
||||
result = signed_pickle_loads(signed_pickle_dumps(obj))
|
||||
assert result["path"] == Path("/tmp/test")
|
||||
assert result["data"] == {1, 2, 3}
|
||||
|
||||
def test_tampered_data_rejected(self):
|
||||
"""Flipping a byte in the data portion causes HMAC failure."""
|
||||
payload = signed_pickle_dumps({"task": "test"})
|
||||
tampered = bytearray(payload)
|
||||
tampered[-1] ^= 0xFF
|
||||
with pytest.raises(ValueError, match="HMAC verification failed"):
|
||||
signed_pickle_loads(bytes(tampered))
|
||||
|
||||
def test_tampered_signature_rejected(self):
|
||||
"""Flipping a byte in the signature portion causes HMAC failure."""
|
||||
payload = signed_pickle_dumps({"task": "test"})
|
||||
tampered = bytearray(payload)
|
||||
tampered[0] ^= 0xFF
|
||||
with pytest.raises(ValueError, match="HMAC verification failed"):
|
||||
signed_pickle_loads(bytes(tampered))
|
||||
|
||||
def test_truncated_payload_rejected(self):
|
||||
"""A payload shorter than HMAC_SIZE is rejected."""
|
||||
with pytest.raises(ValueError, match="too short"):
|
||||
signed_pickle_loads(b"\x00" * (HMAC_SIZE - 1))
|
||||
|
||||
def test_empty_payload_rejected(self):
|
||||
with pytest.raises(ValueError, match="too short"):
|
||||
signed_pickle_loads(b"")
|
||||
|
||||
@override_settings(SECRET_KEY="different-secret-key")
|
||||
def test_wrong_secret_key_rejected(self):
|
||||
"""A message signed with one key cannot be loaded with another."""
|
||||
original_key = b"test-secret-key-do-not-use-in-production"
|
||||
obj = {"task": "test"}
|
||||
data = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
signature = hmac.new(original_key, data, sha256).digest()
|
||||
payload = signature + data
|
||||
with pytest.raises(ValueError, match="HMAC verification failed"):
|
||||
signed_pickle_loads(payload)
|
||||
|
||||
def test_forged_pickle_rejected(self):
|
||||
"""A raw pickle payload (no signature) is rejected."""
|
||||
raw_pickle = pickle.dumps({"task": "test"})
|
||||
# Raw pickle won't have a valid HMAC prefix
|
||||
with pytest.raises(ValueError, match="HMAC verification failed"):
|
||||
signed_pickle_loads(b"\x00" * HMAC_SIZE + raw_pickle)
|
||||
6
uv.lock
generated
6
uv.lock
generated
@@ -1108,14 +1108,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "djangorestframework"
|
||||
version = "3.16.1"
|
||||
version = "3.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user