From 2a350f73015619cae29fc49535a5de6f0a5bcd74 Mon Sep 17 00:00:00 2001 From: lokses Date: Sun, 1 Nov 2026 16:39:30 +0200 Subject: [PATCH] =?UTF-8?q?security:=20=D1=83=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B2=D1=83=D1=85=20Sto?= =?UTF-8?q?red=20XSS=20(JSON=20=D0=B2=20 в пользовательских данных. - OnlyOffice-редактор (documents-editor): имя файла (title) выводилось как var cfg = {{ editor_config|safe }} → перешли на |json_script - РРО (rro.html): поля реестра (notes/model_name/...) в в имени файла и в поле РРО экранируется; safe_filename отклоняет HTML-опасные символы. Прогон: 791 тест, 0 провалов. Co-Authored-By: Claude Opus 4.8 --- acore/settings/base.py | 2 ++ apps/nextcloud_connector/access.py | 8 +++++++- apps/nextcloud_connector/tests.py | 21 +++++++++++++++++++++ apps/nextcloud_connector/views.py | 4 +++- apps/rro/tests.py | 12 ++++++++++++ apps/rro/views.py | 9 ++------- templates/pages/documents-editor.html | 3 ++- templates/pages/rro.html | 6 +++--- 8 files changed, 52 insertions(+), 13 deletions(-) diff --git a/acore/settings/base.py b/acore/settings/base.py index 405b661..a9d52b1 100644 --- a/acore/settings/base.py +++ b/acore/settings/base.py @@ -241,6 +241,8 @@ SESSION_COOKIE_SAMESITE = 'Lax' CSRF_COOKIE_HTTPONLY = True CSRF_COOKIE_SAMESITE = 'Lax' X_FRAME_OPTIONS = 'DENY' +# Запрет MIME-sniffing (важно при отдаче пользовательских файлов/документов). +SECURE_CONTENT_TYPE_NOSNIFF = True # File upload limits DATA_UPLOAD_MAX_MEMORY_SIZE = 15 * 1024 * 1024 # 15 MB diff --git a/apps/nextcloud_connector/access.py b/apps/nextcloud_connector/access.py index 551e9f5..7c4e7fb 100644 --- a/apps/nextcloud_connector/access.py +++ b/apps/nextcloud_connector/access.py @@ -9,6 +9,12 @@ safe_filename() — так исключён обход каталога (path tr from __future__ import annotations import posixpath +import re + +# Запрещённые в имени файла символы: разделители путей, Windows-недопустимые и +# HTML-опасные (< >), кавычки, управляющие (0x00-0x1f). Защита от XSS через имя +# файла и несовместимых имён в хранилище. +_UNSAFE_FILENAME_RE = re.compile(r'[<>:"/\\|?*\x00-\x1f]') from apps.nextcloud_connector.services.sync_service import pharmacy_mountpoint @@ -324,6 +330,6 @@ def safe_filename(name: str | None) -> str | None: return None if posixpath.basename(name) != name: # содержал '/' или иной разделитель return None - if any(c in name for c in ('/', '\\', '\x00')): + if _UNSAFE_FILENAME_RE.search(name): # < > : " | ? * \ / и управляющие return None return name diff --git a/apps/nextcloud_connector/tests.py b/apps/nextcloud_connector/tests.py index 945b9a3..90bc995 100644 --- a/apps/nextcloud_connector/tests.py +++ b/apps/nextcloud_connector/tests.py @@ -116,6 +116,27 @@ class SafeFilenameTests(TestCase): '.hidden', 'x\x00y'): self.assertIsNone(safe_filename(n), f'должно отклонить: {n!r}') + def test_rejects_html_dangerous_chars(self): + # Имя файла с < > " : | ? * недопустимо (защита от XSS в редакторе и + # несовместимых имён в хранилище). + for n in ('x не должно вырваться из '}}, + 'api_js': 'https://ds.example/api.js', + 'back': {'kind': 'documents'}, + 'filename': 'x.docx', 'mode': 'edit', + }) + self.assertNotIn(' и U+2028/2029 — защита от XSS через имя файла). + 'editor_config': config, 'api_js': settings['api_js'], 'filename': resolved['filename'], 'mode': mode, diff --git a/apps/rro/tests.py b/apps/rro/tests.py index 41946ee..dfd87c0 100644 --- a/apps/rro/tests.py +++ b/apps/rro/tests.py @@ -157,6 +157,18 @@ class CashRegisterPortalTests(TestCase): self.assertNotContains(resp, 'id="rro-form-overlay"') self.assertNotContains(resp, 'translateX(100%)') + def test_rro_json_is_xss_safe(self): + """Stored XSS: поле реестра не должно вырваться из ', + ) + self._login() + html = self.client.get('/rro/', HTTP_HX_REQUEST='true').content.decode() + self.assertNotIn(' - {% if rro_form_data_json %}{% endif %} + {{ registers_js|json_script:"rro-registers-json" }} + {% if rro_form_data %}{{ rro_form_data|json_script:"rro-form-data-json" }}{% endif %} {% endif %} {% if not accounting_embed %}{% endif %} @@ -321,7 +321,7 @@ {% if rro_edit_id %} rroOpenEdit({{ rro_edit_id }}); {% endif %} - {% if rro_form_data_json %} + {% if rro_form_data %} (function () { try { applyFormData(JSON.parse(document.getElementById('rro-form-data-json').textContent));