From 4e58d8ae9719cea4f203bdac49afcf56a3989f82 Mon Sep 17 00:00:00 2001
From: joaohortsenado <100957576+joaohortsenado@users.noreply.github.com>
Date: Wed, 5 Aug 2026 17:33:41 -0300
Subject: [PATCH 1/3] =?UTF-8?q?Remove=20limite=20de=20caracteres=20do=20ca?=
=?UTF-8?q?mpo=20Informa=C3=A7=C3=A3o=20Geral=20da=20Casa=20(#3848)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
O campo `CasaLegislativa.informacao_geral` é editado por editor de texto
rico: `CasaLegislativaForm` aplica `attrs={'id': 'texto-rico'}` no widget
e `frontend/src/__global/main.js` instancia o TinyMCE sobre esse id. O
valor submetido, portanto, é HTML, e não texto puro.
Com `max_length=100` no model, o MaxLengthValidator derivado pelo Django
contava os caracteres do HTML, tags inclusive. Um endereço de três linhas
com 87 caracteres visíveis vira 106 caracteres depois dos `
`, e o
salvamento era recusado com "no máximo 100 caracteres (ele possui 106)"
sem que o usuário tivesse digitado 100 caracteres. Quanto mais formatação
o texto tem, mais cedo o erro aparece — e não há como o operador
descobrir sozinho que o limite está sendo consumido por tags que ele não
vê.
O limite nunca teve efeito no banco: `TextField` gera coluna `text` no
PostgreSQL, sem restrição de tamanho. A migration é um no-op em SQL e
serve apenas para manter o state do Django coerente com o model.
Adiciona teste que trava o comportamento e comentário no model
registrando por que o campo não deve receber `max_length` de volta.
OSTicket #824090
---
.../0061_casalegislativa_informacao_geral.py | 19 +++++++++++++
sapl/base/models.py | 4 ++-
sapl/base/tests/test_form.py | 27 +++++++++++++++++++
3 files changed, 49 insertions(+), 1 deletion(-)
create mode 100644 sapl/base/migrations/0061_casalegislativa_informacao_geral.py
diff --git a/sapl/base/migrations/0061_casalegislativa_informacao_geral.py b/sapl/base/migrations/0061_casalegislativa_informacao_geral.py
new file mode 100644
index 000000000..5fec68acd
--- /dev/null
+++ b/sapl/base/migrations/0061_casalegislativa_informacao_geral.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.28 on 2026-08-05 12:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('base', '0060_auto_20240812_1628'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='casalegislativa',
+ name='informacao_geral',
+ field=models.TextField(blank=True,
+ verbose_name='Informação Geral'),
+ ),
+ ]
diff --git a/sapl/base/models.py b/sapl/base/models.py
index 692c55138..b31c532ab 100644
--- a/sapl/base/models.py
+++ b/sapl/base/models.py
@@ -73,8 +73,10 @@ class CasaLegislativa(models.Model):
max_length=100, blank=True, verbose_name=_('HomePage'))
email = models.EmailField(
max_length=100, blank=True, verbose_name=_('E-mail'))
+ # Campo editado via editor de texto rico (TinyMCE), portanto o valor
+ # armazenado é HTML. Não deve ter max_length: as tags consomem o limite
+ # e impedem o salvamento de textos curtos.
informacao_geral = models.TextField(
- max_length=100,
blank=True,
verbose_name=_('Informação Geral'))
diff --git a/sapl/base/tests/test_form.py b/sapl/base/tests/test_form.py
index 35f1c82bf..9464fe39e 100644
--- a/sapl/base/tests/test_form.py
+++ b/sapl/base/tests/test_form.py
@@ -39,3 +39,30 @@ def test_casa_legislativa_form_invalido():
})
assert not form.is_valid()
+
+
+@pytest.mark.django_db(transaction=False)
+def test_casa_legislativa_form_informacao_geral_sem_limite():
+ # O campo é editado por editor de texto rico, então o valor submetido é
+ # HTML: as tags não podem consumir um limite de caracteres.
+ html = ''.join('%s
' % linha for linha in
+ ['Câmara Municipal', 'Rua Osvaldo Cruz, 555 - Centro',
+ 'Expediente das 8h às 12h e das 13h30 às 17h30'])
+ assert len(html) > 100
+
+ form = CasaLegislativaForm(data={'codigo': 'codigo',
+ 'nome': 'nome',
+ 'sigla': 'sg',
+ 'endereco': 'endereco',
+ 'cep': '70000-000',
+ 'municipio': 'municipio',
+ 'uf': 'DF',
+ 'telefone': '33333333',
+ 'fax': '33333333',
+ 'endereco_web': '',
+ 'email': '',
+ 'informacao_geral': html
+ })
+
+ assert form.is_valid(), form.errors
+ assert form.cleaned_data['informacao_geral'] == html
From c82d626540e58b3613e8a06c9a08867a0290af48 Mon Sep 17 00:00:00 2001
From: Edward <9326037+edwardoliveira@users.noreply.github.com>
Date: Tue, 1 Sep 2026 18:34:56 -0300
Subject: [PATCH 2/3] Corrige SQL injection em
DispositivoSearchFragmentFormView (#3856)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Os parâmetros texto, rotulo, num_ta e ano_ta da busca de dispositivos
eram interpolados diretamente na SQL bruta executada via
Dispositivo.objects.raw(), sem nenhum escape (ano_ta nem sequer era
citado entre aspas). Troca as interpolações por placeholders %s com
params, deixando o driver do banco responsável pela escapagem.
Além disso:
- Renomeia a lista de palavras pesquisadas para texto_valores e monta
os fragmentos '%s' repetidos via ['d.texto ~* %s'] * len(texto_valores),
em vez de reatribuir a mesma variável texto ou usar uma comprehension
com variável de loop não utilizada.
- Adiciona sapl.compilacao.utils.valid_int e aplica a tipo_model,
tipo_ta e ano_ta (colunas/PKs inteiras), rejeitando valores não
numéricos com um erro claro em vez de deixar o filtro inválido
seguir adiante. num_ta não é validado dessa forma porque
ta.numero é CharField pesquisado via regex (~*), então aceitar
valores não numéricos ali é esperado.
Claude-Session: https://claude.ai/code/session_01RZAufy4JQKWAaDgfcQByyt
Co-authored-by: Claude Sonnet 5
---
sapl/compilacao/utils.py | 13 ++++++++++++
sapl/compilacao/views.py | 44 ++++++++++++++++++++++++----------------
2 files changed, 39 insertions(+), 18 deletions(-)
diff --git a/sapl/compilacao/utils.py b/sapl/compilacao/utils.py
index aa1793484..c56f618b8 100644
--- a/sapl/compilacao/utils.py
+++ b/sapl/compilacao/utils.py
@@ -1,5 +1,7 @@
import sys
+from django.utils.translation import ugettext_lazy as _
+
DISPOSITIVO_SELECT_RELATED = (
'tipo_dispositivo',
'ta_publicado',
@@ -66,3 +68,14 @@ def get_integrations_view_names():
if 'IntegracaoTaView' in str(base):
result.append(v)
return result
+
+
+def valid_int(value):
+ if value:
+ try:
+ int(value)
+ except (TypeError, ValueError):
+ raise ValueError(
+ _('Valor "%(value)s" não é um número inteiro válido.') % {
+ 'value': value})
+ return value
diff --git a/sapl/compilacao/views.py b/sapl/compilacao/views.py
index 9afbe3201..93a066b3f 100644
--- a/sapl/compilacao/views.py
+++ b/sapl/compilacao/views.py
@@ -49,7 +49,7 @@ from sapl.compilacao.models import (STATUS_TA_EDITION, STATUS_TA_PRIVATE,
VeiculoPublicacao, Vide)
from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED,
DISPOSITIVO_SELECT_RELATED_EDIT,
- get_integrations_view_names)
+ get_integrations_view_names, valid_int)
from sapl.crud.base import RP_DETAIL, RP_LIST, Crud, CrudAux, CrudListView,\
make_pagination
from sapl.settings import BASE_DIR
@@ -3177,14 +3177,14 @@ class DispositivoSearchFragmentFormView(ListView):
result = []
try:
- tipo_model = self.request.GET.get('tipo_model', '')
+ tipo_model = valid_int(self.request.GET.get('tipo_model', ''))
limit = int(self.request.GET.get('max_results', 100))
- tipo_ta = self.request.GET.get('tipo_ta', '')
+ tipo_ta = valid_int(self.request.GET.get('tipo_ta', ''))
num_ta = self.request.GET.get('num_ta', '')
- ano_ta = self.request.GET.get('ano_ta', '')
+ ano_ta = valid_int(self.request.GET.get('ano_ta', ''))
rotulo = self.request.GET.get('rotulo', '')
str_texto = self.request.GET.get('texto', '')
- texto = str_texto.split(' ')
+ texto_valores = str_texto.split(' ')
tipo_resultado = self.request.GET.get('tipo_resultado', '')
tipo_resultado = '' if tipo_resultado == 'False' else tipo_resultado
@@ -3228,20 +3228,24 @@ class DispositivoSearchFragmentFormView(ListView):
AND_CONTROLS = '''AND td.dispositivo_de_alteracao = true
AND td.dispositivo_de_articulacao = true'''
- texto = list(map("d.texto ~* '{}'".format, texto))
+ texto = ['d.texto ~* %s'] * len(texto_valores)
AND_TEXTO_ROTULO = ''
+ texto_rotulo_params = []
if str_texto and rotulo:
- AND_TEXTO_ROTULO = '''AND ( ({BUSCA_TEXTO} AND d.rotulo ~* '{BUSCA_ROTULO}') OR
- ({BUSCA_TEXTO} AND d.rotulo = '' AND dp.rotulo ~* '{BUSCA_ROTULO}')
+ busca_texto = ' AND '.join(texto)
+ AND_TEXTO_ROTULO = '''AND ( ({BUSCA_TEXTO} AND d.rotulo ~* %s) OR
+ ({BUSCA_TEXTO} AND d.rotulo = '' AND dp.rotulo ~* %s)
)'''.format(
- BUSCA_TEXTO=' AND '.join(texto),
- BUSCA_ROTULO=rotulo
+ BUSCA_TEXTO=busca_texto,
)
+ texto_rotulo_params = (
+ texto_valores + [rotulo] + texto_valores + [rotulo])
elif str_texto:
- AND_TEXTO_ROTULO = ' AND %s' % ' AND '.join(texto)
+ AND_TEXTO_ROTULO = ' AND ' + ' AND '.join(texto)
+ texto_rotulo_params = texto_valores
elif rotulo:
- AND_TEXTO_ROTULO = "AND d.rotulo ~* '{BUSCA_ROTULO}'".format(
- BUSCA_ROTULO=rotulo)
+ AND_TEXTO_ROTULO = "AND d.rotulo ~* %s"
+ texto_rotulo_params = [rotulo]
else:
AND_TEXTO_ROTULO = ''
@@ -3288,17 +3292,21 @@ class DispositivoSearchFragmentFormView(ListView):
AND3_TIPO_TA="AND ta.tipo_ta_id = {}".format(
tipo_ta.id) if tipo_ta else '',
- AND2_ANO="AND ta.ano = {}".format(
- ano_ta) if ano_ta else '',
+ AND2_ANO="AND ta.ano = %s" if ano_ta else '',
- AND1_NUMERO="AND ta.numero ~* '{}'".format(
- num_ta) if num_ta else '',
+ AND1_NUMERO="AND ta.numero ~* %s" if num_ta else '',
AND_TEXTO_ROTULO=AND_TEXTO_ROTULO if AND_TEXTO_ROTULO else '',
AND_CONTROLS=AND_CONTROLS if AND_CONTROLS else ''
)
- result = Dispositivo.objects.raw(sql)
+ params = texto_rotulo_params.copy()
+ if num_ta:
+ params.append(num_ta)
+ if ano_ta:
+ params.append(ano_ta)
+
+ result = Dispositivo.objects.raw(sql, params)
r = []
ids = set()
From 55f02aa6397bc613ad5b471b799519e02e53dde5 Mon Sep 17 00:00:00 2001
From: Edward Oliveira
Date: Tue, 1 Sep 2026 18:43:34 -0300
Subject: [PATCH 3/3] Release: 3.1.165-RC3
---
CHANGES.md | 24 ++++++++++++++++++++++++
docker/docker-compose.yaml | 2 +-
sapl/settings.py | 2 +-
sapl/templates/base.html | 2 +-
4 files changed, 27 insertions(+), 3 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index e7b78bbd7..efac508cd 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,28 @@
+3.1.165-RC3 / 2026-09-01
+========================
+
+ * Corrige SQL injection em DispositivoSearchFragmentFormView (#3856)
+ * Remove limite de caracteres do campo Informação Geral da Casa (#3848)
+ * Oculta sessões plenárias não iniciadas da consulta pública (#3845)
+ * hot-fix: Adiciona MIME Media Types opcionais para CSV.
+ * hot-fix: Remove alerta errado
+ * Corrige ordem dos expedientes no PDF do Resumo da Sessão (#3844)
+ * hot-fix: imprime somente contéudo da ocorrência
+ * Fix duplicate normas/matérias in author reports and prevent unfiltered queries
+ * Fix ValueError in audiencia form and AttributeError on HEAD requests
+ * Fix panel clock source and prevent overlapping polling
+ * hot-fix: recover epoch from server always
+ * Corrige polling excessivo e bloqueia não-votantes em /voto-individual/ (#3831)
+ * Hot-fix: corrige cópia de request.data
+ * Release: 3.1.165-RC2
+ * #3821 centralizar geração próximo número para create de matérias legislativas (#3822)
+
+3.1.166-RC0 / 2026-09-01
+========================
+
+
+
3.1.165-RC2 / 2026-03-26
========================
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index dc8559812..46b5bd627 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -33,7 +33,7 @@ services:
networks:
- sapl-net
sapl:
- image: interlegis/sapl:3.1.165-RC2
+ image: interlegis/sapl:3.1.165-RC3
# build:
# context: ../
# dockerfile: ./docker/Dockerfile
diff --git a/sapl/settings.py b/sapl/settings.py
index 4be58ab0c..9c9aa99c7 100644
--- a/sapl/settings.py
+++ b/sapl/settings.py
@@ -43,7 +43,7 @@ ALLOWED_HOSTS = ['*']
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/login/?next='
-SAPL_VERSION = '3.1.165-RC2'
+SAPL_VERSION = '3.1.165-RC3'
if DEBUG:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
diff --git a/sapl/templates/base.html b/sapl/templates/base.html
index 79b285a3a..a11841704 100644
--- a/sapl/templates/base.html
+++ b/sapl/templates/base.html
@@ -200,7 +200,7 @@
Desenvolvido pelo Interlegis em software livre e aberto.
- Release: 3.1.165-RC2
+ Release: 3.1.165-RC3