Browse Source

Recusa upload de arquivos com conteúdo executável pelo navegador

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pull/3865/head
Edward Ribeiro 3 weeks ago
parent
commit
2212c1eaee
  1. 77
      sapl/test_utils.py
  2. 22
      sapl/utils.py

77
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(): def test_listify():
@ -8,3 +12,74 @@ def test_listify():
yield 1 yield 1
yield 2 yield 2
assert [1, 2] == gen() 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', '<office:document-content/>')
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<html><script>alert(1)</script></html>'),
('x.html', b'<html><script>alert(1)</script></html>'),
('x.svg', b'<svg xmlns="http://www.w3.org/2000/svg"></svg>'),
('x.XHTML', b'%PDF-1.4'),
('x.pdf', b'<!DOCTYPE html><html><script>alert(1)</script></html>'),
]
@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)

22
sapl/utils.py

@ -133,6 +133,7 @@ def num_materias_por_tipo(qs, attr_tipo='tipo'):
def validar_arquivo(arquivo, nome_campo): def validar_arquivo(arquivo, nome_campo):
rejeita_conteudo_ativo(arquivo)
if len(arquivo.name) > 200: if len(arquivo.name) > 200:
raise ValidationError( raise ValidationError(
"Certifique-se de que o nome do arquivo no " "Certifique-se de que o nome do arquivo no "
@ -607,6 +608,26 @@ EXTENSOES_ATIVAS = {
'.js', '.mht', '.mhtml', '.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 = ( TIPOS_TEXTO_PERMITIDOS = (
'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.text',
'application/x-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 fabrica_validador_de_tipos_de_arquivo(lista, nome):
def restringe_tipos_de_arquivo(value): def restringe_tipos_de_arquivo(value):
rejeita_conteudo_ativo(value)
filename = value.name if type(value) in ( filename = value.name if type(value) in (
InMemoryUploadedFile, TemporaryUploadedFile InMemoryUploadedFile, TemporaryUploadedFile

Loading…
Cancel
Save