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 = (