KIWITCMS · CVE-2026-77433 · Package
NO FIX YET · SEE WHAT TO DO
The account confirmation endpoint in Kiwi TCMS checks that the redirect target starts with a slash, which lets protocol-relative URLs like //evil.com through. An attacker can send a crafted confirmation link that sends the user to a phishing or malware site after they confirm their account.
first public Nov 9, 2026 · 1 source
Could this affect your app?
Free first check from your website. Connect your repo for the full answer.
Teams that run a self-hosted Kiwi TCMS instance with account registration and email confirmation enabled are exposed, since the flaw sits in the confirmation endpoint. Instances on the fixed release or with no open registration path can skip it; the advisory states that 16.1 and earlier releases lack the mitigation.
Not affected if fixed-version, no-open-registration
Who cares django, kiwitcms, python, self-hosted
pip show kiwitcms and compare it with the fixed version.tcms/kiwi_auth/views.py in the Confirm view for a startswith("/") check on the next parameter.kiwitcms fixed in In Kiwi TCMS version 16.1, the fix for CVE-2026-54724 (open redirect via unvalidated next parameter) in the account confirmation endpoint is incomplete. The validation uses startswith("/") to check if the redirect URL is local, but this is bypassed by protocol-relative URLs like //evil.com which start with / but redirect the browser to an external attacker-controlled domain. 16.1’s incomplete mitigation still allows protocol-relative destinations and earlier releases lack the mitigation.
In tcms/kiwi_auth/views.py, the Confirm view (line ~113):
def get_redirect_url(self, *args, **kwargs):
self.url = self.request.GET.get("next", reverse("core-views-index"))
if not self.url.startswith("/"):
self.url = ()
Check it with your coding agent
It reads this page and tells you if your code is affected.
Are you affected?
Ask from here. InfraStack checks the affected versions against your stack and answers in your inbox.
InfraStack checks the affected versions against your stack and answers in Slack, with the evidence and the fix.
16.2The check startswith("/") passes for //evil.com because it begins with /, but browsers interpret //evil.com as a protocol-relative URL that redirects to https://evil.com (or http://evil.com depending on the current page's protocol).
GET /confirm/<activation_key>/?next=//evil.com/phishing HTTP/1.1
Host: kiwi.example.com
Response:
HTTP/1.1 302 Found
Location: //evil.com/phishing
The browser follows the redirect to https://evil.com/phishing.
Verification:
>>> url = "//evil.com/phishing"
>>> url.startswith("/")
True # passes the check!
An attacker can craft a confirmation link that, after the user confirms their account, redirects them to a phishing page. This is useful for:
The attack requires sending a crafted activation link to a user who is registering, or intercepting/predicting the activation key.
Use Django's url_has_allowed_host_and_scheme() utility instead of a simple startswith check:
from django.utils.http import url_has_allowed_host_and_scheme
def get_redirect_url(self, *args, **kwargs):
self.url = self.request.GET.get("next", reverse("core-views-index"))
if not url_has_allowed_host_and_scheme(
self.url,
allowed_hosts={self.request.get_host()},
require_https=self.request.is_secure(),
):
self.url = reverse("core-views-index")
This properly rejects protocol-relative URLs, absolute URLs to other hosts, and other bypass patterns.
Fixed in Version 16.2
This page as .md · .json · CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
GitHub Advisory Database, CC-BY 4.0. Posts quoted belong to their authors.