Skip to content

Upgrade to Django4#3823

Open
edwardoliveira wants to merge 2 commits into3.1.xfrom
upgrade-sapl-sync-2026-02-25
Open

Upgrade to Django4#3823
edwardoliveira wants to merge 2 commits into3.1.xfrom
upgrade-sapl-sync-2026-02-25

Conversation

@edwardoliveira
Copy link
Contributor

Descrição

Issue Relacionada

Motivação e Contexto

Como Isso Foi Testado?

Capturas de Tela (se apropriado):

Tipos de Mudanças

  • Bug fix (alteração que corrige uma issue e não altera funcionalidades já existentes)
  • Nova feature (alteração que adiciona uma funcionalidade e não altera funcionalidades já existentes)
  • Alteração disruptiva (Breaking change) (Correção ou funcionalidade que causa alteração nas funcionalidades existentes)

Checklist:

  • Eu li o documento de Contribuição (CONTRIBUTING).
  • Meu código segue o estilo de código deste projeto.
  • Minha alteração requer uma alteração na documentação.
  • Eu atualizei a documentação de acordo.
  • Eu adicionei testes para cobrir minhas mudanças.
  • Todos os testes novos e existentes passaram.

self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.META.get('HTTP_REFERER', '/'))
return redirect(request.headers.get('referer', '/'))

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 13 days ago

In general, to fix this issue you must not redirect directly to a URL taken from user input (including headers). Instead, either (a) validate that the URL is safe (e.g., same host, allowed scheme, relative path), or (b) ignore it and redirect to a fixed safe location.

Since this is a Django project, the best low‑impact fix is to use django.utils.http.url_has_allowed_host_and_scheme to validate the referer before using it. If the referer is allowed, redirect to it; otherwise, fall back to '/'. This keeps the existing “go back to where you came from” behavior when safe, but prevents redirection to arbitrary external URLs.

Concretely for sapl/base/views.py:

  • Add url_has_allowed_host_and_scheme to the imports (from django.utils.http), keeping all existing imports unchanged.
  • In both RecuperarSenhaEmailView.get and RecuperarSenhaEmailView.post, replace redirect(request.headers.get('referer', '/')) with logic that:
    • Reads the referer header into a variable.
    • Uses url_has_allowed_host_and_scheme(referer, allowed_hosts={request.get_host()}, require_https=request.is_secure()) to check safety.
    • If safe, redirects to referer; otherwise, redirects to '/'.

This preserves functionality for legitimate in‑site flows and blocks external redirects.

Suggested changeset 1
sapl/base/views.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/sapl/base/views.py b/sapl/base/views.py
--- a/sapl/base/views.py
+++ b/sapl/base/views.py
@@ -23,7 +23,7 @@
 from django.utils import timezone
 from django.utils.decorators import method_decorator
 from django.utils.encoding import force_bytes
-from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
+from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode, url_has_allowed_host_and_scheme
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import (FormView, ListView)
 from django.views.generic.base import RedirectView, TemplateView
@@ -119,7 +119,13 @@
         if not google_recaptcha_configured():
             self.logger.warning(_('Google Recaptcha não configurado!'))
             messages.error(request, _('Google Recaptcha não configurado!'))
-            return redirect(request.headers.get('referer', '/'))
+            referer = request.headers.get('referer', '/')
+            if url_has_allowed_host_and_scheme(
+                    referer,
+                    allowed_hosts={request.get_host()},
+                    require_https=request.is_secure()):
+                return redirect(referer)
+            return redirect('/')
 
         return PasswordResetView.get(self, request, *args, **kwargs)
 
@@ -128,7 +134,13 @@
         if not google_recaptcha_configured():
             self.logger.warning(_('Google Recaptcha não configurado!'))
             messages.error(request, _('Google Recaptcha não configurado!'))
-            return redirect(request.headers.get('referer', '/'))
+            referer = request.headers.get('referer', '/')
+            if url_has_allowed_host_and_scheme(
+                    referer,
+                    allowed_hosts={request.get_host()},
+                    require_https=request.is_secure()):
+                return redirect(referer)
+            return redirect('/')
 
         return PasswordResetView.post(self, request, *args, **kwargs)
 
EOF
@@ -23,7 +23,7 @@
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode, url_has_allowed_host_and_scheme
from django.utils.translation import gettext_lazy as _
from django.views.generic import (FormView, ListView)
from django.views.generic.base import RedirectView, TemplateView
@@ -119,7 +119,13 @@
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.headers.get('referer', '/'))
referer = request.headers.get('referer', '/')
if url_has_allowed_host_and_scheme(
referer,
allowed_hosts={request.get_host()},
require_https=request.is_secure()):
return redirect(referer)
return redirect('/')

return PasswordResetView.get(self, request, *args, **kwargs)

@@ -128,7 +134,13 @@
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.headers.get('referer', '/'))
referer = request.headers.get('referer', '/')
if url_has_allowed_host_and_scheme(
referer,
allowed_hosts={request.get_host()},
require_https=request.is_secure()):
return redirect(referer)
return redirect('/')

return PasswordResetView.post(self, request, *args, **kwargs)

Copilot is powered by AI and may make mistakes. Always verify output.
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.META.get('HTTP_REFERER', '/'))
return redirect(request.headers.get('referer', '/'))

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 13 days ago

In general, to fix untrusted URL redirection, you must not feed raw user input (including headers like Referer) directly into redirect. Instead, either (1) only redirect to a set of known-safe URLs (a whitelist), or (2) validate that the target is a safe relative URL or lies on an allowed host using Django’s URL utilities.

In this specific case, the cleanest fix with minimal behavior change is: keep the idea of “go back to the referring page if possible”, but validate that the Referer is safe. Django already provides url_has_allowed_host_and_scheme for this purpose. We can import it and then, in both get and post methods of RecuperarSenhaEmailView, read referer = request.headers.get('referer', '/'), then check url_has_allowed_host_and_scheme(referer, allowed_hosts={request.get_host()}, require_https=request.is_secure()). If and only if it returns True, redirect to referer; otherwise, fall back to '/'. This preserves existing intended behavior (return to previous page within the same site) while preventing open redirects to external domains.

Concretely:

  • Add an import for url_has_allowed_host_and_scheme from django.utils.http at the top of sapl/base/views.py (expanding the existing import from that module).
  • In RecuperarSenhaEmailView.get, replace return redirect(request.headers.get('referer', '/')) with a small block that:
    • fetches the raw referer,
    • validates it with url_has_allowed_host_and_scheme,
    • chooses a safe_url (referer if valid, '/' otherwise),
    • redirects to safe_url.
  • Do the same replacement in RecuperarSenhaEmailView.post for the identical redirect.

No new methods or complex logic are required; we only add the import and wrap the redirect target with Django’s built-in safety check.

Suggested changeset 1
sapl/base/views.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/sapl/base/views.py b/sapl/base/views.py
--- a/sapl/base/views.py
+++ b/sapl/base/views.py
@@ -23,7 +23,7 @@
 from django.utils import timezone
 from django.utils.decorators import method_decorator
 from django.utils.encoding import force_bytes
-from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
+from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode, url_has_allowed_host_and_scheme
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import (FormView, ListView)
 from django.views.generic.base import RedirectView, TemplateView
@@ -119,7 +119,16 @@
         if not google_recaptcha_configured():
             self.logger.warning(_('Google Recaptcha não configurado!'))
             messages.error(request, _('Google Recaptcha não configurado!'))
-            return redirect(request.headers.get('referer', '/'))
+            referer = request.headers.get('referer', '/')
+            if url_has_allowed_host_and_scheme(
+                referer,
+                allowed_hosts={request.get_host()},
+                require_https=request.is_secure()
+            ):
+                safe_url = referer
+            else:
+                safe_url = '/'
+            return redirect(safe_url)
 
         return PasswordResetView.get(self, request, *args, **kwargs)
 
@@ -128,7 +137,16 @@
         if not google_recaptcha_configured():
             self.logger.warning(_('Google Recaptcha não configurado!'))
             messages.error(request, _('Google Recaptcha não configurado!'))
-            return redirect(request.headers.get('referer', '/'))
+            referer = request.headers.get('referer', '/')
+            if url_has_allowed_host_and_scheme(
+                referer,
+                allowed_hosts={request.get_host()},
+                require_https=request.is_secure()
+            ):
+                safe_url = referer
+            else:
+                safe_url = '/'
+            return redirect(safe_url)
 
         return PasswordResetView.post(self, request, *args, **kwargs)
 
EOF
@@ -23,7 +23,7 @@
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode, url_has_allowed_host_and_scheme
from django.utils.translation import gettext_lazy as _
from django.views.generic import (FormView, ListView)
from django.views.generic.base import RedirectView, TemplateView
@@ -119,7 +119,16 @@
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.headers.get('referer', '/'))
referer = request.headers.get('referer', '/')
if url_has_allowed_host_and_scheme(
referer,
allowed_hosts={request.get_host()},
require_https=request.is_secure()
):
safe_url = referer
else:
safe_url = '/'
return redirect(safe_url)

return PasswordResetView.get(self, request, *args, **kwargs)

@@ -128,7 +137,16 @@
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.headers.get('referer', '/'))
referer = request.headers.get('referer', '/')
if url_has_allowed_host_and_scheme(
referer,
allowed_hosts={request.get_host()},
require_https=request.is_secure()
):
safe_url = referer
else:
safe_url = '/'
return redirect(safe_url)

return PasswordResetView.post(self, request, *args, **kwargs)

Copilot is powered by AI and may make mistakes. Always verify output.
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.META.get('HTTP_REFERER', '/'))
return redirect(request.headers.get('referer', '/'))

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 13 days ago

In general, to fix untrusted URL redirection you must avoid using raw user-controlled data (query parameters, headers like Referer, etc.) directly as redirect targets. Either (1) ignore user input and redirect to a fixed internal URL, (2) maintain a whitelist of allowed redirect paths, or (3) validate the supplied URL with a function such as Django’s url_has_allowed_host_and_scheme and fall back to a safe default if validation fails.

For this specific case in AcompanhamentoDocumentoView.get, the intent is clearly to send the user back to where they came from if Recaptcha is not configured, falling back to / (home page). The simplest safe fix that preserves this behavior is:

  • Read the referer header.
  • Validate it with url_has_allowed_host_and_scheme, restricting to the current host (or Django’s ALLOWED_HOSTS).
  • If valid, redirect to it; otherwise, redirect to '/'.

This avoids open redirects while still letting legitimate same-origin referers work. Concretely:

  • Add an import for url_has_allowed_host_and_scheme from django.utils.http at the top of sapl/protocoloadm/views.py.
  • Replace the return redirect(request.headers.get('referer', '/')) on line 281 with logic that:
    • Extracts referer = request.headers.get('referer')
    • Checks if referer and url_has_allowed_host_and_scheme(referer, allowed_hosts={request.get_host()}, require_https=request.is_secure()):
    • Redirects to referer if valid, else to '/'.

No new methods or classes are required; just this small conditional and the import.

Suggested changeset 1
sapl/protocoloadm/views.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py
--- a/sapl/protocoloadm/views.py
+++ b/sapl/protocoloadm/views.py
@@ -29,6 +29,7 @@
 from django.views.generic.base import RedirectView, TemplateView
 from django.views.generic.edit import FormView
 from django_filters.views import FilterView
+from django.utils.http import url_has_allowed_host_and_scheme
 
 import sapl
 from sapl.base.email_utils import do_envia_email_confirmacao
@@ -278,7 +279,13 @@
         if not google_recaptcha_configured():
             self.logger.warning(_('Google Recaptcha não configurado!'))
             messages.error(request, _('Google Recaptcha não configurado!'))
-            return redirect(request.headers.get('referer', '/'))
+            referer = request.headers.get('referer')
+            if referer and url_has_allowed_host_and_scheme(
+                    referer,
+                    allowed_hosts={request.get_host()},
+                    require_https=request.is_secure()):
+                return redirect(referer)
+            return redirect('/')
 
         pk = self.kwargs['pk']
         documento = DocumentoAdministrativo.objects.get(id=pk)
EOF
@@ -29,6 +29,7 @@
from django.views.generic.base import RedirectView, TemplateView
from django.views.generic.edit import FormView
from django_filters.views import FilterView
from django.utils.http import url_has_allowed_host_and_scheme

import sapl
from sapl.base.email_utils import do_envia_email_confirmacao
@@ -278,7 +279,13 @@
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.headers.get('referer', '/'))
referer = request.headers.get('referer')
if referer and url_has_allowed_host_and_scheme(
referer,
allowed_hosts={request.get_host()},
require_https=request.is_secure()):
return redirect(referer)
return redirect('/')

pk = self.kwargs['pk']
documento = DocumentoAdministrativo.objects.get(id=pk)
Copilot is powered by AI and may make mistakes. Always verify output.
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.META.get('HTTP_REFERER', '/'))
return redirect(request.headers.get('referer', '/'))

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 13 days ago

In general, to fix open redirects you must not feed unvalidated user-controlled data into redirect(). Instead, restrict redirects to either: (a) a fixed internal URL, (b) a choice from a server-side whitelist, or (c) only relative/host-allowed URLs validated with something like Django’s url_has_allowed_host_and_scheme.

The minimal change here is to stop redirecting to an arbitrary Referer header and instead perform a safe redirect. Since the failure case is “Google Recaptcha não configurado!”, the simplest safe behavior is to ignore the Referer and send the user to a known safe page (e.g., /, as already used in the other early returns). This preserves functionality (error message + redirect to home) and removes the tainted data flow.

Concretely:

  • In AcompanhamentoDocumentoView.get, change return redirect(request.headers.get('referer', '/')) to return redirect('/').
  • In AcompanhamentoDocumentoView.post, change return redirect(request.headers.get('referer', '/')) to return redirect('/').

No new imports or helpers are needed; we already import redirect from django.shortcuts.

Suggested changeset 1
sapl/protocoloadm/views.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py
--- a/sapl/protocoloadm/views.py
+++ b/sapl/protocoloadm/views.py
@@ -278,7 +278,7 @@
         if not google_recaptcha_configured():
             self.logger.warning(_('Google Recaptcha não configurado!'))
             messages.error(request, _('Google Recaptcha não configurado!'))
-            return redirect(request.headers.get('referer', '/'))
+            return redirect('/')
 
         pk = self.kwargs['pk']
         documento = DocumentoAdministrativo.objects.get(id=pk)
@@ -297,7 +297,7 @@
         if not google_recaptcha_configured():
             self.logger.warning(_('Google Recaptcha não configurado!'))
             messages.error(request, _('Google Recaptcha não configurado!'))
-            return redirect(request.headers.get('referer', '/'))
+            return redirect('/')
 
         form = AcompanhamentoDocumentoForm(request.POST)
         pk = self.kwargs['pk']
EOF
@@ -278,7 +278,7 @@
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.headers.get('referer', '/'))
return redirect('/')

pk = self.kwargs['pk']
documento = DocumentoAdministrativo.objects.get(id=pk)
@@ -297,7 +297,7 @@
if not google_recaptcha_configured():
self.logger.warning(_('Google Recaptcha não configurado!'))
messages.error(request, _('Google Recaptcha não configurado!'))
return redirect(request.headers.get('referer', '/'))
return redirect('/')

form = AcompanhamentoDocumentoForm(request.POST)
pk = self.kwargs['pk']
Copilot is powered by AI and may make mistakes. Always verify output.
@edwardoliveira edwardoliveira force-pushed the upgrade-sapl-sync-2026-02-25 branch from c1fcbe7 to b0dc138 Compare February 26, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants