From 7b596f9faf0ec8083dea79611b61e7a4a64bff63 Mon Sep 17 00:00:00 2001 From: Edward Oliveira Date: Mon, 12 Oct 2026 11:11:59 -0300 Subject: [PATCH] =?UTF-8?q?Identifica=20conte=C3=BAdo=20ativo=20tamb=C3=A9?= =?UTF-8?q?m=20pelo=20tipo=20MIME=20da=20extens=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- sapl/base/media.py | 23 ++++++++++++----------- sapl/base/tests/test_media.py | 12 +++++++++++- sapl/test_utils.py | 18 +++++++++++++++++- sapl/utils.py | 15 +++++++++++++-- 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/sapl/base/media.py b/sapl/base/media.py index ccb21290a..dc0003f00 100644 --- a/sapl/base/media.py +++ b/sapl/base/media.py @@ -11,15 +11,15 @@ from sapl.protocoloadm.models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo, pode_ver_documento_adm) from sapl.sessao.models import SessaoPlenaria, restringe_sessoes_visiveis -from sapl.utils import EXTENSOES_ATIVAS +from sapl.utils import EXTENSOES_ATIVAS, tipo_ativo X_ACCEL_PREFIX = '/media-interna/' -# Arquivos que dependem de autorização. Inclui as pastas public/ onde -# ficaram os arquivos enviados antes da criação de private/. +# Arquivos que dependem de autorização, em private/ ou em qualquer outra +# pasta (public/ guarda os enviados antes da criação de private/). PROTEGIDO_RE = re.compile( - r'^sapl/(?:private|public/(?:documentoadministrativo|' - r'documentoacessorioadministrativo|proposicao))/') + r'^sapl/[^/]+/(?:documentoadministrativo|' + r'documentoacessorioadministrativo|proposicao)/') def _pode_ver_proposicao(user, proposicao): @@ -75,7 +75,8 @@ def media(request, path): if any(p in ('', '.', '..') for p in partes) or '\x00' in path: raise Http404 - protegido = bool(PROTEGIDO_RE.match(path)) + protegido = path.startswith('sapl/private/') or bool( + PROTEGIDO_RE.match(path)) nome = _nome_protegido(request, path) if protegido else path if path.startswith('sapl/public/sessaoplenaria/') and len(partes) > 4 \ @@ -83,12 +84,12 @@ def media(request, path): raise Http404 nome_arquivo = os.path.basename(nome) - if os.path.splitext(nome_arquivo)[1].lower() in EXTENSOES_ATIVAS: + content_type = (mimetypes.guess_type(nome_arquivo)[0] or + 'application/octet-stream') + disposicao = 'inline' + if os.path.splitext(nome_arquivo)[1].lower() in EXTENSOES_ATIVAS or \ + tipo_ativo(content_type): content_type, disposicao = 'application/octet-stream', 'attachment' - else: - content_type = (mimetypes.guess_type(nome_arquivo)[0] or - 'application/octet-stream') - disposicao = 'inline' if settings.DEBUG: try: diff --git a/sapl/base/tests/test_media.py b/sapl/base/tests/test_media.py index 2b791bc14..48dd71f3a 100644 --- a/sapl/base/tests/test_media.py +++ b/sapl/base/tests/test_media.py @@ -212,7 +212,8 @@ def test_arquivo_publico_passa_por_x_accel(client): @pytest.mark.django_db(transaction=False) -@pytest.mark.parametrize('nome', ['x.svg', 'x.html', 'x.XML', 'x.js']) +@pytest.mark.parametrize('nome', ['x.svg', 'x.html', 'x.XML', 'x.js', + 'x.xht', 'x.rdf', 'x.xslt']) def test_arquivo_publico_ativo_e_baixado(client, nome): response = _get(client, 'sapl/public/normajuridica/2026/1/' + nome) @@ -253,3 +254,12 @@ def test_debug_serve_arquivo(rf, settings, tmp_path): for path in (DOC_PATH, 'sapl/public/normajuridica/1/nao.pdf'): with pytest.raises(Http404): chama(path) + + +@pytest.mark.django_db(transaction=False) +def test_modelo_protegido_fora_de_private_e_public(client): + _config('O') + path = 'sapl/outra/documentoadministrativo/2026/1/texto.pdf' + baker.make(DocumentoAdministrativo, restrito=True, texto_integral=path) + + assert _get(client, path).status_code == 404 diff --git a/sapl/test_utils.py b/sapl/test_utils.py index 9f3002a21..f3956c8fe 100644 --- a/sapl/test_utils.py +++ b/sapl/test_utils.py @@ -2,7 +2,8 @@ from django.core.exceptions import ValidationError import pytest from .utils import (listify, rejeita_conteudo_ativo, - restringe_tipos_de_arquivo_txt, validar_arquivo) + restringe_tipos_de_arquivo_txt, tipo_ativo, + validar_arquivo) def test_listify(): @@ -40,6 +41,8 @@ ATIVOS = [ ('x.html', b''), ('x.svg', b''), ('x.XHTML', b'%PDF-1.4'), + ('x.xht', b'%PDF-1.4\n'), + ('x.rdf', b'%PDF-1.4'), ('x.pdf', b''), ] @@ -83,3 +86,16 @@ def test_validar_arquivo_ignora_arquivo_ja_gravado(): antigo = FieldFile(None, campo, 'sapl/private/documentoadministrativo/x.html') rejeita_conteudo_ativo(antigo) + + +@pytest.mark.parametrize('mime,ativo', [ + ('text/html', True), ('application/xhtml+xml', True), + ('application/rdf+xml', True), ('text/xml', True), + ('application/javascript', True), ('image/svg+xml', True), + ('application/pdf', False), ('image/png', False), ('text/csv', False), + ('application/vnd.openxmlformats-officedocument.wordprocessingml.document', + False), + (None, False), +]) +def test_tipo_ativo(mime, ativo): + assert tipo_ativo(mime) is ativo diff --git a/sapl/utils.py b/sapl/utils.py index 8fff1a785..295aa8170 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -41,6 +41,7 @@ from django.utils.translation import ugettext_lazy as _ import django_filters from easy_thumbnails import source_generators import magic +import mimetypes import requests from unipath.path import Path from xlsxwriter.workbook import Workbook @@ -614,18 +615,28 @@ MIMES_ATIVOS = { } +def tipo_ativo(mime): + """MIME que o navegador renderiza como documento capaz de rodar script.""" + if not mime: + return False + mime = mime.lower() + return (mime in MIMES_ATIVOS or 'html' in mime or 'javascript' in mime + or mime.endswith('/xml') or mime.endswith('+xml')) + + 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: + if os.path.splitext(arquivo.name)[1].lower() in EXTENSOES_ATIVAS or \ + tipo_ativo(mimetypes.guess_type(arquivo.name)[0]): 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: + if tipo_ativo(mime): raise ValidationError(_('Tipo de arquivo não permitido.')) TIPOS_TEXTO_PERMITIDOS = (