diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index ca05c71b7..79a88872f 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -38,6 +38,7 @@ django-prometheus==2.2.0
asn1crypto==1.5.1
XlsxWriter==3.2.0
+nh3==0.2.22
setuptools==80.9.0
diff --git a/sapl/base/receivers.py b/sapl/base/receivers.py
index f0e5365c6..a783b106b 100644
--- a/sapl/base/receivers.py
+++ b/sapl/base/receivers.py
@@ -8,6 +8,7 @@ from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core import serializers
from django.core.files.uploadedfile import InMemoryUploadedFile, UploadedFile
+from django.db.models import TextField
from django.db.models.fields.files import FileField
from django.db.models.signals import post_delete, post_save, \
post_migrate, pre_save, pre_migrate
@@ -22,11 +23,15 @@ from sapl.decorators import receiver_multi_senders
from sapl.materia.models import Tramitacao
from sapl.parlamentares.models import Parlamentar
from sapl.protocoloadm.models import TramitacaoAdministrativo
+from sapl.sanitize import sanitize_field
from sapl.utils import get_base_url, models_with_gr_for_model
models_with_gr_for_autor = models_with_gr_for_model(Autor)
+SAPL_APP_LABELS = frozenset(
+ app.rsplit('.', 1)[-1] for app in settings.SAPL_APPS)
+
@receiver_multi_senders(post_save, senders=models_with_gr_for_autor)
def handle_update_autor_related(sender, **kwargs):
@@ -477,6 +482,43 @@ def signed_files_extraction_pre_save_signal(sender, instance, **kwargs):
signed_files_extraction_function(sender, instance, **kwargs)
+# Cache dos TextField por modelo: a introspecção de _meta.fields a cada save
+# pesa mais que a própria sanitização, e compilacao salva Dispositivo em laço.
+_text_fields_cache = {}
+
+
+def get_text_fields(model):
+ try:
+ return _text_fields_cache[model]
+ except KeyError:
+ fields = [f.name for f in model._meta.fields
+ if isinstance(f, TextField)]
+ _text_fields_cache[model] = fields
+ return fields
+
+
+@receiver(pre_save, dispatch_uid='sanitize_textfields_pre_save_signal')
+def sanitize_textfields_pre_save_signal(sender, instance, **kwargs):
+ """Remove HTML/JavaScript perigoso de todo TextField dos modelos do SAPL.
+
+ Cobre forms, a API do drfautoapi, o admin e o shell num único ponto.
+ Ver sapl.sanitize para as políticas por campo.
+ """
+ if sender._meta.app_label not in SAPL_APP_LABELS:
+ return
+
+ for fieldname in get_text_fields(sender):
+ value = getattr(instance, fieldname, None)
+ if not value:
+ continue
+ sanitized = sanitize_field(sender, fieldname, value)
+ if sanitized != value:
+ setattr(instance, fieldname, sanitized)
+
+
@receiver(pre_migrate, dispatch_uid='disconnect_signals_pre_migrate')
def disconnect_signals_pre_migrate(*args, **kwargs):
+ # sanitize_textfields_pre_save_signal não é desconectado aqui de propósito:
+ # é barato e idempotente, e desconectá-lo deixaria sem proteção qualquer
+ # migrate rodado no mesmo processo (é o caso da suíte de testes).
pre_save.disconnect(dispatch_uid='signed_files_extraction_pre_save_signal')
diff --git a/sapl/base/templatetags/common_tags.py b/sapl/base/templatetags/common_tags.py
index 84f61b64f..d2c741ee6 100644
--- a/sapl/base/templatetags/common_tags.py
+++ b/sapl/base/templatetags/common_tags.py
@@ -10,6 +10,7 @@ from sapl.base.models import AppConfig
from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa, Proposicao
from sapl.norma.models import NormaJuridica
from sapl.parlamentares.models import Filiacao
+from sapl.sanitize import sanitize_html
from sapl.sessao.models import SessaoPlenaria
from sapl.utils import filiacao_data, SEPARADOR_HASH_PROPOSICAO, is_report_allowed
@@ -396,6 +397,17 @@ def render_chunk_vendors(extension=None):
return ''
+@register.filter(is_safe=True)
+@stringfilter
+def sanitize(value):
+ """Renderiza HTML de campo rico (TinyMCE) sem script nem URL perigosa.
+
+ Substitui o |safe nos campos que legitimamente guardam HTML. Protege
+ também as linhas gravadas antes da sanitização no pre_save.
+ """
+ return mark_safe(sanitize_html(value, rich=True))
+
+
@register.filter(is_safe=True)
@stringfilter
def dont_break_out(value):
diff --git a/sapl/base/tests/test_sanitize.py b/sapl/base/tests/test_sanitize.py
new file mode 100644
index 000000000..cfa2766e3
--- /dev/null
+++ b/sapl/base/tests/test_sanitize.py
@@ -0,0 +1,163 @@
+import pytest
+from model_bakery import baker
+
+from sapl.crispy_layout_mixin import get_field_display
+from sapl.lexml.models import LexmlProvedor
+from sapl.protocoloadm.models import TramitacaoAdministrativo
+from sapl.sanitize import sanitize_field, sanitize_html, sanitize_scope
+from sapl.sessao.models import ExpedienteSessao
+
+
+def test_plain_remove_marcacao_e_preserva_texto():
+ assert sanitize_html('Ciente') == 'Ciente'
+ assert sanitize_html('Encaminhado ao setor') == \
+ 'Encaminhado ao setor'
+ assert sanitize_html('') == ''
+ assert sanitize_html('
centro
', + '| c |
centro
', rich=True) + + saida = sanitize_html( + '| c |