From b1054189c2a7ff03aa5f009a6618f90cb7ce0de8 Mon Sep 17 00:00:00 2001 From: Edward Oliveira Date: Tue, 13 Oct 2026 16:43:55 -0300 Subject: [PATCH] =?UTF-8?q?Envia=20ao=20Django=20s=C3=B3=20os=20arquivos?= =?UTF-8?q?=20de=20m=C3=ADdia=20que=20dependem=20de=20autoriza=C3=A7=C3=A3?= =?UTF-8?q?o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Os demais arquivos de /media/ voltam a ser servidos direto pelo nginx, com nosniff e download forçado para extensões ativas. Co-Authored-By: Claude Opus 5.5 --- docker/config/nginx/sapl.conf | 32 ++++++++++++++++++++++++++++++-- sapl/base/media.py | 4 ++++ sapl/base/tests/test_media.py | 20 +++++++++++++++++++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/docker/config/nginx/sapl.conf b/docker/config/nginx/sapl.conf index 44b339173..7e0802ef1 100644 --- a/docker/config/nginx/sapl.conf +++ b/docker/config/nginx/sapl.conf @@ -49,8 +49,36 @@ server { alias /var/interlegis/sapl/collected_static/; } - # /media/ não é servido direto do disco: cai em location / e o Django - # (sapl.base.media) autoriza e devolve X-Accel-Redirect para cá. + # Arquivos que dependem de autorização vão ao Django (sapl.base.media), + # que devolve X-Accel-Redirect para /media-interna/. Manter a lista em + # sincronia com PROTEGIDO_RE e MODELOS_DE_SESSAO (teste em + # sapl/base/tests/test_media.py). Os dois regex ficam no nível do server + # e antes de location /media/: um regex aninhado em /media/ teria + # precedência sobre este. + location ~* ^/media/sapl/(private/|[^/]+/(documentoadministrativo|documentoacessorioadministrativo|proposicao|sessaoplenaria|orador|oradorexpediente|oradorordemdia|justificativaausencia)/) { + proxy_set_header X-Request-ID $req_id; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://sapl_server; + } + + # Demais arquivos são públicos e servidos direto do disco; conteúdo que o + # navegador executaria como página é forçado a download. + location ~* ^/media/.+\.(html?|xhtml|xht|shtml|svgz?|xml|xsl|xslt|rdf|js|mjs|mht|mhtml|rss|atom|mml)$ { + root /var/interlegis/sapl; + types { } + default_type application/octet-stream; + add_header Content-Disposition attachment always; + add_header X-Content-Type-Options nosniff always; + } + + location /media/ { + root /var/interlegis/sapl; + add_header X-Content-Type-Options nosniff always; + } + location /media-interna/ { internal; alias /var/interlegis/sapl/media/; diff --git a/sapl/base/media.py b/sapl/base/media.py index 87d636ee0..9b91027c2 100644 --- a/sapl/base/media.py +++ b/sapl/base/media.py @@ -17,6 +17,10 @@ from sapl.utils import EXTENSOES_ATIVAS, tipo_ativo X_ACCEL_PREFIX = '/media-interna/' +# O nginx do container só envia para cá os caminhos de PROTEGIDO_RE e de +# MODELOS_DE_SESSAO; o resto de /media/ sai direto do disco. Ao incluir um +# modelo aqui, incluí-lo também em docker/config/nginx/sapl.conf. + # 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( diff --git a/sapl/base/tests/test_media.py b/sapl/base/tests/test_media.py index 24ceb7156..ee7a27446 100644 --- a/sapl/base/tests/test_media.py +++ b/sapl/base/tests/test_media.py @@ -1,3 +1,7 @@ +import os +import re + +from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import Permission @@ -8,7 +12,7 @@ from django.utils import timezone from model_bakery import baker import pytest -from sapl.base.media import media +from sapl.base.media import MODELOS_DE_SESSAO, RESOLVEDORES, media from sapl.base.models import AppConfig, Autor from sapl.materia.models import Proposicao from sapl.protocoloadm.models import (DocumentoAcessorioAdministrativo, @@ -345,3 +349,17 @@ def test_proposicao_texto(client, settings, tmp_path, estado, perfil, else: assert response.status_code == 302 assert response['Location'] == '/proposicao/%s' % pk + + +def test_nginx_envia_ao_django_todos_os_caminhos_protegidos(): + caminho = os.path.join(settings.PROJECT_DIR, 'docker', 'config', + 'nginx', 'sapl.conf') + with open(caminho) as f: + conf = f.read() + regex = re.search(r'location ~\*? \^/media/sapl/\(private/\|' + r'\[\^/\]\+/\(([a-z|]+)\)/\)', conf) + + assert regex, 'location de arquivos protegidos não encontrada' + no_nginx = set(regex.group(1).split('|')) + assert set(RESOLVEDORES) | set(MODELOS_DE_SESSAO) <= no_nginx + assert conf.index(regex.group(0)) < conf.index('location /media/ {')