{"id":"CVE-2026-77433","kind":"vulnerability","aliases":["CVE-2026-77433","GHSA-gcwf-c25f-p9rv"],"related":[],"title":"Kiwi TCMS account confirmation open redirect through protocol-relative URLs","description":"## 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","summary":"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.","severity":"medium","cvss":6.1,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"kiwitcms","fixed":[],"affected":"<= 12.4","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-601","django","kiwitcms","package","python","self-hosted"],"not_affected_if":["fixed-version","no-open-registration"],"how_to_check":"- Check the installed version: `pip show kiwitcms` and compare it with the fixed version.\n- Look at `tcms/kiwi_auth/views.py` in the `Confirm` view for a `startswith(\"/\")` check on the `next` parameter.","fix":"- `kiwitcms` fixed in `16.2`","status":"open","published_at":"2026-11-09T23:24:14+00:00","first_seen_at":"2026-11-09T23:28:35+00:00","triaged_at":"2026-11-09T23:29:27+00:00","updated_at":"2026-11-09T23:29:27+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-11-09T23:29:27Z","model":"claude-haiku-5-5","output":{"fix":"- `kiwitcms` fixed in `16.2`","reach":"internet","title":"Kiwi TCMS: account confirmation link can redirect users to external sites","impact":"medium","summary":"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.","audience":"businesses","category":"package","nicknames":[],"who_cares":["kiwitcms","self-hosted","python","django"],"confidence":0.8,"fix_status":"available","needs_auth":"no","worst_case":"users are redirected to attacker-controlled phishing pages after confirmation","analyst_note":"","how_to_check":"- Check the installed version: `pip show kiwitcms` and compare it with the fixed version.\n- Look at `tcms/kiwi_auth/views.py` in the `Confirm` view for a `startswith(\"/\")` check on the `next` parameter.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["fixed-version","no-open-registration"],"who_is_affected":"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.","user_interaction":"yes","product_ecosystem":"","needs_special_setup":"unknown","severity_if_unknown":"medium"},"cost_usd":0.000835,"input_tokens":5554,"output_tokens":560,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-2026':13A,21A,91C '-54724':92C '-601':25B '-77433':14A,22A '/confirm':229C '/evil.com':56B,128C,197C,205C '/evil.com/phishing':231C,240C,252C '/phishing':249C '113':167C '16.1':86C,143C '16.2':414C '302':237C 'absolut':402C 'account':3A,34B,80B,101C,272C 'action':295C 'activ':317C,328C 'allow':148C,337C,353C,376C,381C 'arg':173C,362C 'attack':59B,140C,260C,289C,312C 'attacker-control':139C 'begin':200C 'browser':135C,203C,242C 'bypass':121C,409C 'c25f':18A 'check':40B,111C,193C,257C,347C 'code':160C 'concept':227C 'confirm':4A,35B,64B,78B,102C,164C,264C,270C 'control':141C 'core':180C,189C,369C,392C 'core-views-index':179C,188C,368C,391C 'craft':63B,262C,316C 'credenti':283C 'current':221C 'cve':12A,20A,90C 'cwe':24B 'def':168C,357C 'deliveri':292C 'depend':218C 'destin':152C 'django':26B,333C 'django.utils.http':349C 'domain':142C,290C 'earlier':154C 'endpoint':36B,103C 'engin':297C 'evil.com':215C,217C,248C 'evil.com/phishing':247C 'extern':138C 'fake':285C 'fix':88C,331C,411C 'follow':243C 'found':238C 'gcwf':17A 'get':169C,228C,358C 'ghsa':16A 'ghsa-gcwf-c25f-p9rv':15A 'host':32B,233C,338C,354C,377C,382C,384C,406C 'http/1.1':232C,236C 'https':386C 'impact':258C 'import':350C 'incomplet':105C,145C 'index':182C,191C,371C,394C 'instanc':310C 'instead':342C 'intercepting/predicting':326C 'interpret':204C 'key':329C 'kiwi':1A,38B,83C,308C 'kiwi.example.com':234C 'kiwitcm':27B 'kiwitcms':23A 'kwarg':174C,363C 'lack':156C 'legitim':307C 'let':50B 'like':55B,127C 'line':166C 'link':65B,265C,301C,318C 'local':117C 'locat':239C 'login':286C 'malwar':74B,291C 'mitig':146C,158C 'next':97C,177C,230C,366C 'open':5A,93C 'p9rv':19A 'packag':28B 'page':222C,278C,287C 'paramet':98C 'pass':195C,255C 'pattern':410C 'phish':72B,277C,284C 'proof':225C 'proper':396C 'protocol':9A,52B,124C,150C,209C,224C,399C 'protocol-rel':8A,51B,123C,149C,208C,398C 'python':29B 'redirect':6A,43B,94C,114C,133C,170C,213C,245C,273C,359C 'regist':324C 'reject':397C 'relat':10A,53B,125C,151C,210C,400C 'releas':155C 'requir':313C,385C 'respons':235C 'revers':178C,187C,367C,390C 'scheme':340C,356C,379C 'secur':388C 'self':31B,172C,361C 'self-host':30B 'self.request.get':383C 'self.request.get.get':176C,365C 'self.request.is':387C 'self.url':175C,186C,364C,380C,389C 'self.url.startswith':185C 'send':61B,67B,314C 'simpl':345C 'site':75B 'slash':48B 'social':296C 'start':45B,130C 'startswith':109C,194C,346C 'still':147C 'suggest':330C 'summari':81C 'target':44B 'tcms':2A,39B,84C,309C 'tcms/kiwi_auth/views.py':162C 'true':254C 'trust':294C,299C 'unvalid':96C 'url':11A,54B,115C,126C,171C,211C,251C,335C,351C,360C,374C,401C,403C 'url.startswith':253C 'use':108C,281C,332C 'user':69B,269C,298C,321C 'util':341C 'valid':107C 'verif':250C 'version':85C,413C 'via':95C 'view':165C,181C,190C,370C,393C 'vulner':159C","nicknames":[],"mitigations":[],"who_affected":"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.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"businesses","plain_action":null,"pkeys":["kiwitcms"],"ptokens":["kiwitcms"],"labels":["cwe601","django","kiwitcms","medium","package","pypi","python","selfhosted"],"campaign_type":null,"search_a":"'-2026':13A,21A '-77433':14A,22A 'account':3A 'c25f':18A 'confirm':4A 'cve':12A,20A 'gcwf':17A 'ghsa':16A 'ghsa-gcwf-c25f-p9rv':15A 'kiwi':1A 'kiwitcms':23A 'open':5A 'p9rv':19A 'protocol':9A 'protocol-rel':8A 'redirect':6A 'relat':10A 'tcms':2A 'url':11A","search_b":"'-601':25B '/evil.com':56B 'account':34B,80B 'attack':59B 'check':40B 'confirm':35B,64B,78B 'craft':63B 'cwe':24B 'django':26B 'endpoint':36B 'host':32B 'kiwi':38B 'kiwitcm':27B 'let':50B 'like':55B 'link':65B 'malwar':74B 'packag':28B 'phish':72B 'protocol':52B 'protocol-rel':51B 'python':29B 'redirect':43B 'relat':53B 'self':31B 'self-host':30B 'send':61B,67B 'site':75B 'slash':48B 'start':45B 'target':44B 'tcms':39B 'url':54B 'user':69B","search_c":"'-2026':91C '-54724':92C '/confirm':229C '/evil.com':128C,197C,205C '/evil.com/phishing':231C,240C,252C '/phishing':249C '113':167C '16.1':86C,143C '16.2':414C '302':237C 'absolut':402C 'account':101C,272C 'action':295C 'activ':317C,328C 'allow':148C,337C,353C,376C,381C 'arg':173C,362C 'attack':140C,260C,289C,312C 'attacker-control':139C 'begin':200C 'browser':135C,203C,242C 'bypass':121C,409C 'check':111C,193C,257C,347C 'code':160C 'concept':227C 'confirm':102C,164C,264C,270C 'control':141C 'core':180C,189C,369C,392C 'core-views-index':179C,188C,368C,391C 'craft':262C,316C 'credenti':283C 'current':221C 'cve':90C 'def':168C,357C 'deliveri':292C 'depend':218C 'destin':152C 'django':333C 'django.utils.http':349C 'domain':142C,290C 'earlier':154C 'endpoint':103C 'engin':297C 'evil.com':215C,217C,248C 'evil.com/phishing':247C 'extern':138C 'fake':285C 'fix':88C,331C,411C 'follow':243C 'found':238C 'get':169C,228C,358C 'host':233C,338C,354C,377C,382C,384C,406C 'http/1.1':232C,236C 'https':386C 'impact':258C 'import':350C 'incomplet':105C,145C 'index':182C,191C,371C,394C 'instanc':310C 'instead':342C 'intercepting/predicting':326C 'interpret':204C 'key':329C 'kiwi':83C,308C 'kiwi.example.com':234C 'kwarg':174C,363C 'lack':156C 'legitim':307C 'like':127C 'line':166C 'link':265C,301C,318C 'local':117C 'locat':239C 'login':286C 'malwar':291C 'mitig':146C,158C 'next':97C,177C,230C,366C 'open':93C 'page':222C,278C,287C 'paramet':98C 'pass':195C,255C 'pattern':410C 'phish':277C,284C 'proof':225C 'proper':396C 'protocol':124C,150C,209C,224C,399C 'protocol-rel':123C,149C,208C,398C 'redirect':94C,114C,133C,170C,213C,245C,273C,359C 'regist':324C 'reject':397C 'relat':125C,151C,210C,400C 'releas':155C 'requir':313C,385C 'respons':235C 'revers':178C,187C,367C,390C 'scheme':340C,356C,379C 'secur':388C 'self':172C,361C 'self.request.get':383C 'self.request.get.get':176C,365C 'self.request.is':387C 'self.url':175C,186C,364C,380C,389C 'self.url.startswith':185C 'send':314C 'simpl':345C 'social':296C 'start':130C 'startswith':109C,194C,346C 'still':147C 'suggest':330C 'summari':81C 'tcms':84C,309C 'tcms/kiwi_auth/views.py':162C 'true':254C 'trust':294C,299C 'unvalid':96C 'url':115C,126C,171C,211C,251C,335C,351C,360C,374C,401C,403C 'url.startswith':253C 'use':108C,281C,332C 'user':269C,298C,321C 'util':341C 'valid':107C 'verif':250C 'version':85C,413C 'via':95C 'view':165C,181C,190C,370C,393C 'vulner':159C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-09T23:24:14+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-gcwf-c25f-p9rv","url":"https://github.com/advisories/GHSA-gcwf-c25f-p9rv","severity":"medium","published_at":"2026-11-09T23:24:14+00:00","modified_at":"2026-11-09T23:24:17+00:00","first_seen_at":"2026-11-09T23:28:35+00:00","fetched_at":"2026-11-09T23:38:07+00:00","note":null}],"documents":[{"id":111196,"url":"https://api.github.com/repos/kiwitcms/Kiwi/security-advisories/GHSA-gcwf-c25f-p9rv","title":"Maintainer's advisory: api.github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: api.github.com"},{"id":111197,"url":"https://github.com/kiwitcms/Kiwi/security/advisories/GHSA-gcwf-c25f-p9rv","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":111198,"url":"https://github.com/kiwitcms/Kiwi/security/advisories/GHSA-hmj5-jm8h-h9fh","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":111199,"url":"https://github.com/kiwitcms/Kiwi/pull/4458","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":111200,"url":"https://github.com/kiwitcms/Kiwi/commit/65d8f7c2743d7f06a4fca77c060b9d561f02f948","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":111201,"url":"https://github.com/kiwitcms/Kiwi/releases/tag/v16.2","title":"Release: github.com","kind":"release","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Release: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}