mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-10-02 01:42:50 -05:00
Merge branch 'dev' into feature-remote-ocr-2
This commit is contained in:
@@ -54,7 +54,7 @@ class HttpRemoteUserMiddleware(PersistentRemoteUserMiddleware):
|
||||
|
||||
header = settings.HTTP_REMOTE_USER_HEADER_NAME
|
||||
|
||||
def process_request(self, request: HttpRequest) -> None:
|
||||
def __call__(self, request: HttpRequest) -> None:
|
||||
# If remote user auth is enabled only for the frontend, not the API,
|
||||
# then we need dont want to authenticate the user for API requests.
|
||||
if (
|
||||
@@ -62,8 +62,8 @@ class HttpRemoteUserMiddleware(PersistentRemoteUserMiddleware):
|
||||
and "paperless.auth.PaperlessRemoteUserAuthentication"
|
||||
not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]
|
||||
):
|
||||
return
|
||||
return super().process_request(request)
|
||||
return self.get_response(request)
|
||||
return super().__call__(request)
|
||||
|
||||
|
||||
class PaperlessRemoteUserAuthentication(authentication.RemoteUserAuthentication):
|
||||
|
@@ -214,31 +214,3 @@ def audit_log_check(app_configs, **kwargs):
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@register()
|
||||
def check_postgres_version(app_configs, **kwargs):
|
||||
"""
|
||||
Django 5.2 removed PostgreSQL 13 support and thus it will be removed in
|
||||
a future Paperless-ngx version. This check can be removed eventually.
|
||||
See https://docs.djangoproject.com/en/5.2/releases/5.2/#dropped-support-for-postgresql-13
|
||||
"""
|
||||
db_conn = connections["default"]
|
||||
result = []
|
||||
if db_conn.vendor == "postgresql":
|
||||
try:
|
||||
with db_conn.cursor() as cursor:
|
||||
cursor.execute("SHOW server_version;")
|
||||
version = cursor.fetchone()[0]
|
||||
if version.startswith("13"):
|
||||
return [
|
||||
Warning(
|
||||
"PostgreSQL 13 is deprecated and will not be supported in a future Paperless-ngx release.",
|
||||
hint="Upgrade to PostgreSQL 14 or newer.",
|
||||
),
|
||||
]
|
||||
except Exception: # pragma: no cover
|
||||
# Don't block checks on version query failure
|
||||
pass
|
||||
|
||||
return result
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
|
||||
import magic
|
||||
from allauth.mfa.adapter import get_adapter as get_mfa_adapter
|
||||
from allauth.mfa.models import Authenticator
|
||||
from allauth.mfa.totp.internal.auth import TOTP
|
||||
@@ -12,6 +13,7 @@ from rest_framework import serializers
|
||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||
|
||||
from paperless.models import ApplicationConfiguration
|
||||
from paperless.validators import reject_dangerous_svg
|
||||
from paperless_mail.serialisers import ObfuscatedPasswordField
|
||||
|
||||
logger = logging.getLogger("paperless.settings")
|
||||
@@ -206,6 +208,11 @@ class ApplicationConfigurationSerializer(serializers.ModelSerializer):
|
||||
instance.app_logo.delete()
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def validate_app_logo(self, file):
|
||||
if file and magic.from_buffer(file.read(2048), mime=True) == "image/svg+xml":
|
||||
reject_dangerous_svg(file)
|
||||
return file
|
||||
|
||||
class Meta:
|
||||
model = ApplicationConfiguration
|
||||
fields = "__all__"
|
||||
|
@@ -9,7 +9,6 @@ from documents.tests.utils import DirectoriesMixin
|
||||
from documents.tests.utils import FileSystemAssertsMixin
|
||||
from paperless.checks import audit_log_check
|
||||
from paperless.checks import binaries_check
|
||||
from paperless.checks import check_postgres_version
|
||||
from paperless.checks import debug_mode_check
|
||||
from paperless.checks import paths_check
|
||||
from paperless.checks import settings_values_check
|
||||
@@ -263,39 +262,3 @@ class TestAuditLogChecks(TestCase):
|
||||
("auditlog table was found but audit log is disabled."),
|
||||
msg.msg,
|
||||
)
|
||||
|
||||
|
||||
class TestPostgresVersionCheck(TestCase):
|
||||
@mock.patch("paperless.checks.connections")
|
||||
def test_postgres_13_warns(self, mock_connections):
|
||||
mock_connection = mock.MagicMock()
|
||||
mock_connection.vendor = "postgresql"
|
||||
mock_cursor = mock.MagicMock()
|
||||
mock_cursor.__enter__.return_value.fetchone.return_value = ["13.11"]
|
||||
mock_connection.cursor.return_value = mock_cursor
|
||||
mock_connections.__getitem__.return_value = mock_connection
|
||||
|
||||
warnings = check_postgres_version(None)
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertIn("PostgreSQL 13 is deprecated", warnings[0].msg)
|
||||
|
||||
@mock.patch("paperless.checks.connections")
|
||||
def test_postgres_14_passes(self, mock_connections):
|
||||
mock_connection = mock.MagicMock()
|
||||
mock_connection.vendor = "postgresql"
|
||||
mock_cursor = mock.MagicMock()
|
||||
mock_cursor.__enter__.return_value.fetchone.return_value = ["14.10"]
|
||||
mock_connection.cursor.return_value = mock_cursor
|
||||
mock_connections.__getitem__.return_value = mock_connection
|
||||
|
||||
warnings = check_postgres_version(None)
|
||||
self.assertEqual(warnings, [])
|
||||
|
||||
@mock.patch("paperless.checks.connections")
|
||||
def test_non_postgres_skipped(self, mock_connections):
|
||||
mock_connection = mock.MagicMock()
|
||||
mock_connection.vendor = "sqlite"
|
||||
mock_connections.__getitem__.return_value = mock_connection
|
||||
|
||||
warnings = check_postgres_version(None)
|
||||
self.assertEqual(warnings, [])
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import override_settings
|
||||
from rest_framework import status
|
||||
@@ -91,6 +92,7 @@ class TestRemoteUser(DirectoriesMixin, APITestCase):
|
||||
|
||||
@override_settings(
|
||||
REST_FRAMEWORK={
|
||||
**settings.REST_FRAMEWORK,
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"rest_framework.authentication.BasicAuthentication",
|
||||
"rest_framework.authentication.TokenAuthentication",
|
||||
|
@@ -1,5 +1,3 @@
|
||||
from pathlib import Path
|
||||
|
||||
from allauth.account import views as allauth_account_views
|
||||
from allauth.mfa.base import views as allauth_mfa_views
|
||||
from allauth.socialaccount import views as allauth_social_account_views
|
||||
@@ -13,7 +11,6 @@ from django.urls import re_path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.generic import RedirectView
|
||||
from django.views.static import serve
|
||||
from drf_spectacular.views import SpectacularAPIView
|
||||
from drf_spectacular.views import SpectacularSwaggerView
|
||||
from rest_framework.routers import DefaultRouter
|
||||
@@ -45,6 +42,7 @@ from documents.views import UnifiedSearchViewSet
|
||||
from documents.views import WorkflowActionViewSet
|
||||
from documents.views import WorkflowTriggerViewSet
|
||||
from documents.views import WorkflowViewSet
|
||||
from documents.views import serve_logo
|
||||
from paperless.consumers import StatusConsumer
|
||||
from paperless.views import ApplicationConfigurationViewSet
|
||||
from paperless.views import DisconnectSocialAccountView
|
||||
@@ -267,11 +265,7 @@ urlpatterns = [
|
||||
# TODO: with localization, this is even worse! :/
|
||||
),
|
||||
# App logo
|
||||
re_path(
|
||||
r"^logo(?P<path>.*)$",
|
||||
serve,
|
||||
kwargs={"document_root": Path(settings.MEDIA_ROOT) / "logo"},
|
||||
),
|
||||
re_path(r"^logo(?:/(?P<filename>.+))?/?$", serve_logo, name="app_logo"),
|
||||
# allauth
|
||||
path(
|
||||
"accounts/",
|
||||
|
102
src/paperless/validators.py
Normal file
102
src/paperless/validators.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from lxml import etree
|
||||
|
||||
ALLOWED_SVG_TAGS: set[str] = {
|
||||
"svg",
|
||||
"g",
|
||||
"path",
|
||||
"rect",
|
||||
"circle",
|
||||
"ellipse",
|
||||
"line",
|
||||
"polyline",
|
||||
"polygon",
|
||||
"text",
|
||||
"tspan",
|
||||
"defs",
|
||||
"linearGradient",
|
||||
"radialGradient",
|
||||
"stop",
|
||||
"clipPath",
|
||||
"use",
|
||||
"title",
|
||||
"desc",
|
||||
}
|
||||
|
||||
ALLOWED_SVG_ATTRIBUTES: set[str] = {
|
||||
"id",
|
||||
"class",
|
||||
"style",
|
||||
"d",
|
||||
"fill",
|
||||
"fill-rule",
|
||||
"stroke",
|
||||
"stroke-width",
|
||||
"stroke-linecap",
|
||||
"stroke-linejoin",
|
||||
"stroke-miterlimit",
|
||||
"stroke-dasharray",
|
||||
"stroke-dashoffset",
|
||||
"stroke-opacity",
|
||||
"transform",
|
||||
"x",
|
||||
"y",
|
||||
"cx",
|
||||
"cy",
|
||||
"r",
|
||||
"rx",
|
||||
"ry",
|
||||
"width",
|
||||
"height",
|
||||
"x1",
|
||||
"y1",
|
||||
"x2",
|
||||
"y2",
|
||||
"gradientTransform",
|
||||
"gradientUnits",
|
||||
"offset",
|
||||
"stop-color",
|
||||
"stop-opacity",
|
||||
"clip-path",
|
||||
"viewBox",
|
||||
"preserveAspectRatio",
|
||||
"href",
|
||||
"xlink:href",
|
||||
"font-family",
|
||||
"font-size",
|
||||
"font-weight",
|
||||
"text-anchor",
|
||||
"xmlns",
|
||||
"xmlns:xlink",
|
||||
}
|
||||
|
||||
|
||||
def reject_dangerous_svg(file):
|
||||
"""
|
||||
Rejects SVG files that contain dangerous tags or attributes.
|
||||
Raises ValidationError if unsafe content is found.
|
||||
See GHSA-6p53-hqqw-8j62
|
||||
"""
|
||||
try:
|
||||
parser = etree.XMLParser(resolve_entities=False)
|
||||
file.seek(0)
|
||||
tree = etree.parse(file, parser)
|
||||
root = tree.getroot()
|
||||
except etree.XMLSyntaxError:
|
||||
raise ValidationError("Invalid SVG file.")
|
||||
|
||||
for element in root.iter():
|
||||
tag = etree.QName(element.tag).localname.lower()
|
||||
if tag not in ALLOWED_SVG_TAGS:
|
||||
raise ValidationError(f"Disallowed SVG tag: <{tag}>")
|
||||
|
||||
for attr_name, attr_value in element.attrib.items():
|
||||
attr_name_lower = attr_name.lower()
|
||||
if attr_name_lower not in ALLOWED_SVG_ATTRIBUTES:
|
||||
raise ValidationError(f"Disallowed SVG attribute: {attr_name}")
|
||||
|
||||
if attr_name_lower in {
|
||||
"href",
|
||||
"xlink:href",
|
||||
} and attr_value.strip().lower().startswith("javascript:"):
|
||||
raise ValidationError(f"Disallowed javascript: URI in {attr_name}")
|
@@ -1,6 +1,6 @@
|
||||
from typing import Final
|
||||
|
||||
__version__: Final[tuple[int, int, int]] = (2, 17, 1)
|
||||
__version__: Final[tuple[int, int, int]] = (2, 18, 0)
|
||||
# Version string like X.Y.Z
|
||||
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
||||
# Version string like X.Y
|
||||
|
Reference in New Issue
Block a user