Merge commit from fork

* Security: prevent XSS with storage path template rendering

* Security: prevent XSS svg uploads

* Security: force attachment disposition for logo

* Add suggestions from code review

* Improve SVG validation with allowlist for tags and attributes
This commit is contained in:
shamoon
2025-08-16 07:34:00 -07:00
committed by GitHub
parent 42bdbc1b2d
commit b1c406680f
9 changed files with 179 additions and 10 deletions

View File

@@ -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 magic.from_buffer(file.read(2048), mime=True) == "image/svg+xml":
reject_dangerous_svg(file)
return file
class Meta:
model = ApplicationConfiguration
fields = "__all__"

View File

@@ -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"},
),
path("logo/<path:filename>", serve_logo, name="app_logo"),
# allauth
path(
"accounts/",

102
src/paperless/validators.py Normal file
View 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}")