Browse Source

Envia ao Django só os arquivos de mídia que dependem de autorização

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 <noreply@anthropic.com>
pull/3865/head
Edward Ribeiro 2 weeks ago
parent
commit
b1054189c2
  1. 32
      docker/config/nginx/sapl.conf
  2. 4
      sapl/base/media.py
  3. 20
      sapl/base/tests/test_media.py

32
docker/config/nginx/sapl.conf

@ -49,8 +49,36 @@ server {
alias /var/interlegis/sapl/collected_static/; alias /var/interlegis/sapl/collected_static/;
} }
# /media/ não é servido direto do disco: cai em location / e o Django # Arquivos que dependem de autorização vão ao Django (sapl.base.media),
# (sapl.base.media) autoriza e devolve X-Accel-Redirect para cá. # 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/ { location /media-interna/ {
internal; internal;
alias /var/interlegis/sapl/media/; alias /var/interlegis/sapl/media/;

4
sapl/base/media.py

@ -17,6 +17,10 @@ from sapl.utils import EXTENSOES_ATIVAS, tipo_ativo
X_ACCEL_PREFIX = '/media-interna/' 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 # Arquivos que dependem de autorização, em private/ ou em qualquer outra
# pasta (public/ guarda os enviados antes da criação de private/). # pasta (public/ guarda os enviados antes da criação de private/).
PROTEGIDO_RE = re.compile( PROTEGIDO_RE = re.compile(

20
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 import get_user_model
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
@ -8,7 +12,7 @@ from django.utils import timezone
from model_bakery import baker from model_bakery import baker
import pytest 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.base.models import AppConfig, Autor
from sapl.materia.models import Proposicao from sapl.materia.models import Proposicao
from sapl.protocoloadm.models import (DocumentoAcessorioAdministrativo, from sapl.protocoloadm.models import (DocumentoAcessorioAdministrativo,
@ -345,3 +349,17 @@ def test_proposicao_texto(client, settings, tmp_path, estado, perfil,
else: else:
assert response.status_code == 302 assert response.status_code == 302
assert response['Location'] == '/proposicao/%s' % pk 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/ {')

Loading…
Cancel
Save