From 2212c1eaee058992e18cbf47193bc7016dda1a14 Mon Sep 17 00:00:00 2001 From: Edward Oliveira Date: Mon, 12 Oct 2026 10:58:25 -0300 Subject: [PATCH] =?UTF-8?q?Recusa=20upload=20de=20arquivos=20com=20conte?= =?UTF-8?q?=C3=BAdo=20execut=C3=A1vel=20pelo=20navegador?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- sapl/test_utils.py | 77 +++++++++++++++++++++++++++++++++++++++++++++- sapl/utils.py | 22 +++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/sapl/test_utils.py b/sapl/test_utils.py index 925b0ce8a..9f3002a21 100644 --- a/sapl/test_utils.py +++ b/sapl/test_utils.py @@ -1,4 +1,8 @@ -from .utils import listify +from django.core.exceptions import ValidationError +import pytest + +from .utils import (listify, rejeita_conteudo_ativo, + restringe_tipos_de_arquivo_txt, validar_arquivo) def test_listify(): @@ -8,3 +12,74 @@ def test_listify(): yield 1 yield 2 assert [1, 2] == gen() + + +def _upload(nome, conteudo): + import io + from django.core.files.uploadedfile import InMemoryUploadedFile + return InMemoryUploadedFile(io.BytesIO(conteudo), 'arquivo', nome, None, + len(conteudo), None) + + +def _odt(): + import io + import zipfile + buf = io.BytesIO() + with zipfile.ZipFile(buf, 'w') as z: + z.writestr(zipfile.ZipInfo('mimetype'), + 'application/vnd.oasis.opendocument.text') + z.writestr('content.xml', '') + return buf.getvalue() + + +PNG = (b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01' + b'\x08\x06\x00\x00\x00\x1f\x15\xc4\x89') + +ATIVOS = [ + ('x.html', b'%PDF-1.4\n'), + ('x.html', b''), + ('x.svg', b''), + ('x.XHTML', b'%PDF-1.4'), + ('x.pdf', b''), +] + + +@pytest.mark.parametrize('nome,conteudo', ATIVOS) +def test_validar_arquivo_rejeita_conteudo_ativo(nome, conteudo): + with pytest.raises(ValidationError): + validar_arquivo(_upload(nome, conteudo), 'Arquivo') + + +@pytest.mark.parametrize('nome,conteudo', ATIVOS) +def test_restringe_tipos_rejeita_conteudo_ativo(nome, conteudo): + with pytest.raises(ValidationError): + restringe_tipos_de_arquivo_txt(_upload(nome, conteudo)) + + +@pytest.mark.parametrize('nome,conteudo', [ + ('a.pdf', b'%PDF-1.4\n%%EOF'), + ('a.csv', b'nome;valor\nx;1\n'), + ('a.odt', _odt()), + ('a.png', PNG), + ('a.xlsx', b'PK\x03\x04' + b'\x00' * 64), +]) +def test_validar_arquivo_aceita_tipos_comuns(nome, conteudo): + validar_arquivo(_upload(nome, conteudo), 'Arquivo') + + +@pytest.mark.parametrize('nome,conteudo', [ + ('a.pdf', b'%PDF-1.4\n%%EOF'), + ('a.csv', b'nome;valor\nx;1\n'), + ('a.odt', _odt()), +]) +def test_restringe_tipos_aceita_documentos(nome, conteudo): + restringe_tipos_de_arquivo_txt(_upload(nome, conteudo)) + + +def test_validar_arquivo_ignora_arquivo_ja_gravado(): + from django.db.models.fields.files import FieldFile + from sapl.protocoloadm.models import DocumentoAdministrativo + campo = DocumentoAdministrativo._meta.get_field('texto_integral') + antigo = FieldFile(None, campo, + 'sapl/private/documentoadministrativo/x.html') + rejeita_conteudo_ativo(antigo) diff --git a/sapl/utils.py b/sapl/utils.py index 615fb2f11..8fff1a785 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -133,6 +133,7 @@ def num_materias_por_tipo(qs, attr_tipo='tipo'): def validar_arquivo(arquivo, nome_campo): + rejeita_conteudo_ativo(arquivo) if len(arquivo.name) > 200: raise ValidationError( "Certifique-se de que o nome do arquivo no " @@ -607,6 +608,26 @@ EXTENSOES_ATIVAS = { '.js', '.mht', '.mhtml', } +MIMES_ATIVOS = { + 'text/html', 'application/xhtml+xml', 'image/svg+xml', 'text/xml', + 'application/xml', 'application/javascript', 'text/javascript', +} + + +def rejeita_conteudo_ativo(arquivo): + """Recusa uploads que o navegador executaria como página, pela extensão + ou pelo conteúdo. Arquivos já gravados não são checados, para não impedir + a edição de registros antigos.""" + if not isinstance(arquivo, UploadedFile): + return + if os.path.splitext(arquivo.name)[1].lower() in EXTENSOES_ATIVAS: + raise ValidationError(_('Tipo de arquivo não permitido.')) + arquivo.seek(0) + mime = magic.from_buffer(arquivo.read(2048), mime=True) + arquivo.seek(0) + if mime in MIMES_ATIVOS: + raise ValidationError(_('Tipo de arquivo não permitido.')) + TIPOS_TEXTO_PERMITIDOS = ( 'application/vnd.oasis.opendocument.text', 'application/x-vnd.oasis.opendocument.text', @@ -654,6 +675,7 @@ TIPOS_IMG_PERMITIDOS = ( def fabrica_validador_de_tipos_de_arquivo(lista, nome): def restringe_tipos_de_arquivo(value): + rejeita_conteudo_ativo(value) filename = value.name if type(value) in ( InMemoryUploadedFile, TemporaryUploadedFile