{"id":"CVE-2026-77433","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-77433","summary":"Kiwi TCMS account confirmation open redirect through protocol-relative URLs","details":"## Summary\n\nIn 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.\n\n## Vulnerable Code\n\nIn `tcms/kiwi_auth/views.py`, the `Confirm` view (line ~113):\n\n    def get_redirect_url(self, *args, **kwargs):\n        self.url = self.request.GET.get(\"next\", reverse(\"core-views-index\"))\n        if not self.url.startswith(\"/\"):\n            self.url = reverse(\"core-views-index\")\n\nThe 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).\n\n## Proof of Concept\n\n    GET /confirm/<activation_key>/?next=//evil.com/phishing HTTP/1.1\n    Host: kiwi.example.com\n\nResponse:\n\n    HTTP/1.1 302 Found\n    Location: //evil.com/phishing\n\nThe browser follows the redirect to `https://evil.com/phishing`.\n\nVerification:\n\n    >>> url = \"//evil.com/phishing\"\n    >>> url.startswith(\"/\")\n    True  # passes the check!\n\n## Impact\n\nAn attacker can craft a confirmation link that, after the user confirms their account, redirects them to a phishing page. This is useful for:\n\n- Credential phishing (fake login page on attacker domain)\n- Malware delivery after trusted action\n- Social engineering (user trusts the link because it's from the legitimate Kiwi TCMS instance)\n\nThe attack requires sending a crafted activation link to a user who is registering, or intercepting/predicting the activation key.\n\n## Suggested Fix\n\nUse Django's `url_has_allowed_host_and_scheme()` utility instead of a simple `startswith` check:\n\n    from django.utils.http import url_has_allowed_host_and_scheme\n\n    def get_redirect_url(self, *args, **kwargs):\n        self.url = self.request.GET.get(\"next\", reverse(\"core-views-index\"))\n        if not url_has_allowed_host_and_scheme(\n            self.url,\n            allowed_hosts={self.request.get_host()},\n            require_https=self.request.is_secure(),\n        ):\n            self.url = reverse(\"core-views-index\")\n\nThis properly rejects protocol-relative URLs, absolute URLs to other hosts, and other bypass patterns.\n\nFixed in Version 16.2","published":"2026-11-09T23:24:14Z","modified":"2026-11-09T23:30:04.658688800Z","cvss":{"score":6.1,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"PyPI","name":"kiwitcms","fixedVersion":null}],"fix":{"url":"https://github.com/kiwitcms/Kiwi/pull/4458","label":"kiwitcms/Kiwi#4458"},"references":[{"type":"WEB","url":"https://github.com/kiwitcms/Kiwi/security/advisories/GHSA-gcwf-c25f-p9rv"},{"type":"WEB","url":"https://github.com/kiwitcms/Kiwi/security/advisories/GHSA-hmj5-jm8h-h9fh"},{"type":"WEB","url":"https://github.com/kiwitcms/Kiwi/pull/4458"},{"type":"WEB","url":"https://github.com/kiwitcms/Kiwi/commit/65d8f7c2743d7f06a4fca77c060b9d561f02f948"},{"type":"PACKAGE","url":"https://github.com/kiwitcms/Kiwi"},{"type":"WEB","url":"https://github.com/kiwitcms/Kiwi/releases/tag/v16.2"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-11-09T23:30:04.658688800Z"}}