GHSA-gcwf-c25f-p9rv: Kiwi TCMS account confirmation open redirect through protocol-relative URLs
Summary
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.
Vulnerable Code
In tcms/kiwiauth/views.py, the Confirm view (line ~113):
def getredirecturl(self, args, kwargs): self.url = self.request.GET.get("next", reverse("core-views-index")) if not self.url.startswith("/"): self.url = reverse("core-views-index")
The 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).
Proof of Concept
GET /confirm/<activationkey>/?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!
Impact
An attacker can craft a confirmation link that, after the user confirms their account, redirects them to a phishing page. This is useful for:
- Credential phishing (fake login page on attacker domain) - Malware delivery after trusted action - Social engineering (user trusts the link because it's from the legitimate Kiwi TCMS instance)
The attack requires sending a crafted activation link to a user who is registering, or intercepting/predicting the activation key.
Suggested Fix
Use Django's urlhasallowedhostandscheme() utility instead of a simple startswith check:
from django.utils.http import urlhasallowedhostandscheme
def getredirecturl(self, args, kwargs): self.url = self.request.GET.get("next", reverse("core-views-index")) if not urlhasallowedhostandscheme( self.url, allowedhosts={self.request.gethost()}, requirehttps=self.request.issecure(), ): 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
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
Kiwi TCMSto a version that resolves this vulnerability.Fixed in 16.2
Event History
Frequently Asked Questions
Which releases are affected?
Kiwi TCMS 16.1 remains affected because its validation can be bypassed with protocol-relative URLs. Earlier releases are also affected because they do not include the attempted mitigation.
What does exploitation require from an attacker?
An attacker needs to induce a user to visit an account-confirmation URL with a next parameter set to a protocol-relative destination, such as //evil.com/phishing. The browser then follows the 302 redirect to the attacker-controlled external site.
How can we identify attempted exploitation?
Review requests to the confirmation endpoint for next values beginning with two slashes, such as /confirm/<activation_key>/?next=//evil.com/phishing. A corresponding response with a Location header beginning with // indicates the redirect was accepted.