{"id":"CVE-2026-109283","kind":"vulnerability","aliases":["CVE-2026-109283","GHSA-2rr4-q6pg-m5g3"],"related":[],"title":"Revoked users keep signing in via token exchange when roles are only in the ID token","description":"# Revoked users keep signing in via token exchange when roles are only in the ID token\r\n\r\n## Summary\r\nThe OAuth token exchange endpoint lets a user who already has a linked OAuth account swap a provider access token for an Open WebUI session. Since 0.11.1 it applies the OAuth role policy, but it only reads roles from the provider's userinfo response, and it only refuses a user when a roles claim is found. With an identity provider that sends roles only in the ID token (Microsoft Entra ID app roles are the common case), token exchange sees no roles and keeps the user's existing role, so a user whose provider role no longer satisfies the policy, and who is correctly refused at browser login, can still get a working session.\r\n\r\n## Preconditions\r\n- `ENABLE_OAUTH_TOKEN_EXCHANGE=true` (default `false`).\r\n- `ENABLE_OAUTH_ROLE_MANAGEMENT=true` (default `false`), with `OAUTH_ALLOWED_ROLES` (default `user,admin`) not containing `*`.\r\n- The identity provider puts the roles claim only in the ID token and not in the userinfo response.\r\n- The attacker already has an Open WebUI account linked to their provider identity, and can still obtain a valid access token from the provider (their provider account is active, only their role changed).\r\n\r\nDeployments with either setting off, or whose provider returns roles in userinfo, are not affected.\r\n\r\n## Impact\r\nA user the operator meant to lock out through the OAuth role policy keeps signing in through token exchange and keeps the role they held before, together with everything that role can reach. The attacker gains nothing beyond their previous access: an empty roles claim can neither grant admin nor change the role. Exploitation needs the non-default configuration above and a previously provisioned account. Browser SSO login is not affected and refuses the user as intended.\r\n\r\n## Fix\r\nFixed in 10d1cfe63, released in 0.11.4. Token exchange now also reads the roles claim from the exchanged token, and when role management is on and no roles claim can be read at all, it refuses the exchange.\r\n\r\n## Root cause\r\n- Component: the OAuth token exchange endpoint (`POST /api/v1/auths/oauth/{provider}/token/exchange`) and the OAuth role policy it calls.\r\n- Affected builds: 0.11.1 through 0.11.3, the releases that added the role policy to token exchange.\r\n\r\nThe role policy was written for browser login, where the ID token claims are merged into the user data before the policy runs, so \"no roles claim\" there means the provider genuinely sent none and keeping the current role is the documented behaviour. Token exchange never receives an ID token and builds the user data from userinfo alone, so for providers that only put roles in the ID token the policy always saw an empty claim and fell through to keeping the existing role instead of refusing.\r\n\r\n## Proof of concept\r\nReproduced on the 0.11.3 and 0.11.4 PyPI packages with a local OIDC provider that puts `roles` only in the ID token, `ENABLE_OAUTH_TOKEN_EXCHANGE=true`, `ENABLE_OAUTH_ROLE_MANAGEMENT=true` and an `OAUTH_ALLOWED_ROLES` list.\r\n\r\n1. Sign in once through SSO while holding an allowed role, so the account is linked.\r\n2. At the provider, change the user's role to one outside the allowed list.\r\n3. Browser SSO login: refused with 403 on both versions.\r\n4. Obtain an access token from the provider and send `POST /api/v1/auths/oauth/{provider}/token/exchange` with `{\"token\": \"<access token>\"}`.\r\n5. On 0.11.3 the request returns 200 with a session token that works on `GET /api/v1/auths/`. On 0.11.4 it returns 403.\r\n\r\nControl: a user still holding an allowed role is allowed on both versions.\r\n\r\n## Credits\r\n- `manus-pi`: reported the role policy bypass on token exchange for providers that send roles only in the ID token.","summary":"When Open WebUI's OAuth token exchange is enabled with role management, a user whose identity provider role was changed to a disallowed one can still get a working session if roles appear only in the ID token. The attacker keeps the role they held before, and gains nothing beyond their previous access.","severity":"medium","cvss":5.4,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"open-webui","fixed":["0.11.4"],"affected":">= 0.11.1, < 0.11.4","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["ai-tooling","cwe-863","entra-id","oauth","open-webui","python","self-hosted"],"not_affected_if":["browser-login-only","default-config","provider-returns-roles-in-userinfo"],"how_to_check":"- Check that `ENABLE_OAUTH_TOKEN_EXCHANGE` and `ENABLE_OAUTH_ROLE_MANAGEMENT` are both set to `true`.\n- Check whether `OAUTH_ALLOWED_ROLES` is set and does not contain `*`.\n- Check whether your identity provider sends the roles claim only in the ID token and not in the userinfo response (Microsoft Entra ID app roles are the common case).\n- Check your installed version with `pip show open-webui`; affected builds are `>= 0.11.1, < 0.11.4`.","fix":"- `open-webui` (pypi): move to `0.11.4`.","status":"open","published_at":"2026-09-27T16:14:42+00:00","first_seen_at":"2026-11-09T22:53:28+00:00","triaged_at":"2026-11-09T23:01:09+00:00","updated_at":"2026-11-09T23:01:09+00:00","category":"ai-tooling","confidence":0.85,"triage":{"at":"2026-11-09T23:01:09Z","model":"claude-haiku-5-5","output":{"fix":"- `open-webui` (pypi): move to `0.11.4`.","reach":"internet","title":"Open WebUI: revoked users keep sign-in through OAuth token exchange","impact":"medium","summary":"When Open WebUI's OAuth token exchange is enabled with role management, a user whose identity provider role was changed to a disallowed one can still get a working session if roles appear only in the ID token. The attacker keeps the role they held before, and gains nothing beyond their previous access.","audience":"businesses","category":"ai-tooling","nicknames":[],"who_cares":["open-webui","python","self-hosted","oauth","entra-id"],"confidence":0.85,"fix_status":"available","needs_auth":"yes","worst_case":"Revoked user keeps a session with their former role's access","analyst_note":"","how_to_check":"- Check that `ENABLE_OAUTH_TOKEN_EXCHANGE` and `ENABLE_OAUTH_ROLE_MANAGEMENT` are both set to `true`.\n- Check whether `OAUTH_ALLOWED_ROLES` is set and does not contain `*`.\n- Check whether your identity provider sends the roles claim only in the ID token and not in the userinfo response (Microsoft Entra ID app roles are the common case).\n- Check your installed version with `pip show open-webui`; affected builds are `>= 0.11.1, < 0.11.4`.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["default-config","browser-login-only","provider-returns-roles-in-userinfo"],"who_is_affected":"Only deployments that turn on OAuth token exchange and OAuth role management, with an identity provider that puts roles only in the ID token, are exposed. An attacker must already have a linked account and an active provider login whose role has since been removed from the allowed list. Deployments with either setting off, or whose provider returns roles in userinfo, are not affected.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.000942,"input_tokens":5847,"output_tokens":714,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-109283':19A,27A '-2026':18A,26A '-863':35B '/api/v1/auths':671C '/api/v1/auths/oauth':447C,651C '/token/exchange':449C,653C '0.11.1':143C,459C '0.11.3':461C,565C,658C '0.11.4':406C,567C,673C '1':599C '10d1cfe63':403C '2':615C '200':662C '2rr4':22A '3':630C '4':640C '403':636C,676C '5':656C 'access':99B,135C,292C,362C,643C 'account':131C,280C,299C,387C,612C 'activ':301C 'add':465C 'admin':252C,370C 'affect':320C,393C,457C 'ai':32B 'ai-tool':31B 'allow':248C,596C,608C,628C,683C,686C 'alon':529C 'alreadi':126C,275C 'also':410C 'alway':543C 'app':188C 'appear':79B 'appli':145C 'attack':86B,274C,356C 'behaviour':514C 'beyond':96B,359C 'browser':223C,388C,478C,631C 'build':458C,523C 'bypass':698C 'call':456C 'case':193C 'caus':439C 'chang':66B,305C,372C,619C 'claim':170C,261C,366C,414C,428C,484C,498C,547C 'common':192C 'compon':440C 'concept':561C 'configur':381C 'contain':254C 'control':677C 'correct':220C 'credit':690C 'current':509C 'cve':17A,25A 'cwe':34B 'data':490C,526C 'default':237C,244C,250C,380C 'deploy':306C 'disallow':69B 'document':513C 'either':308C 'empti':364C,546C 'enabl':55B,232C,239C,583C,588C 'endpoint':121C,445C 'entra':37B,186C 'entra-id':36B 'everyth':350C 'exchang':8A,53B,107C,120C,195C,235C,340C,408C,417C,437C,444C,471C,516C,586C,701C 'exist':204C,554C 'exploit':375C 'fals':238C,245C 'fell':549C 'fix':400C,401C 'found':172C 'gain':94B,357C 'genuin':503C 'get':73B,227C,670C 'ghsa':21A 'ghsa-2rr4-q6pg-m5g3':20A 'grant':369C 'held':91B,346C 'hold':606C,681C 'host':46B 'id':15A,38B,83B,114C,183C,187C,265C,482C,520C,539C,581C,710C 'ident':62B,175C,256C,285C 'impact':321C 'instead':556C 'intend':399C 'keep':3A,87B,102C,200C,335C,342C,507C,552C 'let':122C 'link':129C,281C,614C 'list':598C,629C 'local':572C 'lock':328C 'login':224C,390C,479C,633C 'longer':213C 'm5g3':24A 'manag':58B,242C,422C,591C 'manus':692C 'manus-pi':691C 'mean':500C 'meant':326C 'merg':486C 'microsoft':185C 'need':376C 'neither':368C 'never':517C 'non':379C 'non-default':378C 'none':505C 'noth':95B,358C 'oauth':39B,51B,118C,130C,147C,233C,240C,247C,332C,442C,452C,584C,589C,595C 'obtain':289C,641C 'oidc':573C 'one':70B,625C 'open':29A,41B,48B,139C,278C 'open-webui':28A,40B 'oper':325C 'outsid':626C 'packag':569C 'pi':693C 'polici':149C,216C,334C,454C,468C,474C,493C,542C,697C 'post':446C,650C 'precondit':231C 'previous':98B,361C,385C 'proof':559C 'provid':63B,134C,157C,176C,210C,257C,284C,296C,298C,313C,448C,502C,532C,574C,618C,647C,652C,703C 'provis':386C 'put':258C,535C,576C 'pypi':568C 'python':43B 'q6pg':23A 'reach':354C 'read':153C,411C,431C 'receiv':518C 'refus':164C,221C,395C,435C,558C,634C 'releas':404C,463C 'report':694C 'reproduc':562C 'request':660C 'respons':160C,272C 'return':314C,661C,675C 'revok':1A,100C 'role':10A,57B,64B,78B,89B,109C,148C,154C,169C,179C,189C,198C,205C,211C,241C,249C,260C,304C,315C,333C,344C,352C,365C,374C,413C,421C,427C,453C,467C,473C,497C,510C,536C,555C,577C,590C,597C,609C,623C,684C,696C,706C 'root':438C 'run':494C 'satisfi':214C 'saw':544C 'see':196C 'self':45B 'self-host':44B 'send':178C,649C,705C 'sent':504C 'session':76B,141C,230C,665C 'set':309C 'sign':4A,103C,336C,600C 'sinc':142C 'sso':389C,604C,632C 'still':72B,226C,288C,680C 'summari':116C 'swap':132C 'togeth':348C 'token':7A,16A,52B,84B,106C,115C,119C,136C,184C,194C,234C,266C,293C,339C,407C,418C,443C,470C,483C,515C,521C,540C,582C,585C,644C,655C,666C,700C,711C 'tool':33B 'true':236C,243C,587C,592C 'user':2A,60B,101C,124C,166C,202C,208C,251C,323C,397C,489C,525C,621C,679C 'userinfo':159C,271C,317C,528C 'valid':291C 'version':639C,689C 'via':6A,105C 'webui':30A,42B,49B,140C,279C 'whose':61B,209C,312C 'work':75B,229C,668C 'written':476C","nicknames":[],"mitigations":[],"who_affected":"Only deployments that turn on OAuth token exchange and OAuth role management, with an identity provider that puts roles only in the ID token, are exposed. An attacker must already have a linked account and an active provider login whose role has since been removed from the allowed list. Deployments with either setting off, or whose provider returns roles in userinfo, are not affected.","tier":"B","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"businesses","plain_action":null,"pkeys":["openwebui"],"ptokens":["open","webui"],"labels":["aitooling","cwe863","entraid","medium","oauth","openwebui","pypi","python","selfhosted"],"campaign_type":null,"search_a":"'-109283':19A,27A '-2026':18A,26A '2rr4':22A 'cve':17A,25A 'exchang':8A 'ghsa':21A 'ghsa-2rr4-q6pg-m5g3':20A 'id':15A 'keep':3A 'm5g3':24A 'open':29A 'open-webui':28A 'q6pg':23A 'revok':1A 'role':10A 'sign':4A 'token':7A,16A 'user':2A 'via':6A 'webui':30A","search_b":"'-863':35B 'access':99B 'ai':32B 'ai-tool':31B 'appear':79B 'attack':86B 'beyond':96B 'chang':66B 'cwe':34B 'disallow':69B 'enabl':55B 'entra':37B 'entra-id':36B 'exchang':53B 'gain':94B 'get':73B 'held':91B 'host':46B 'id':38B,83B 'ident':62B 'keep':87B 'manag':58B 'noth':95B 'oauth':39B,51B 'one':70B 'open':41B,48B 'open-webui':40B 'previous':98B 'provid':63B 'python':43B 'role':57B,64B,78B,89B 'self':45B 'self-host':44B 'session':76B 'still':72B 'token':52B,84B 'tool':33B 'user':60B 'webui':42B,49B 'whose':61B 'work':75B","search_c":"'/api/v1/auths':671C '/api/v1/auths/oauth':447C,651C '/token/exchange':449C,653C '0.11.1':143C,459C '0.11.3':461C,565C,658C '0.11.4':406C,567C,673C '1':599C '10d1cfe63':403C '2':615C '200':662C '3':630C '4':640C '403':636C,676C '5':656C 'access':135C,292C,362C,643C 'account':131C,280C,299C,387C,612C 'activ':301C 'add':465C 'admin':252C,370C 'affect':320C,393C,457C 'allow':248C,596C,608C,628C,683C,686C 'alon':529C 'alreadi':126C,275C 'also':410C 'alway':543C 'app':188C 'appli':145C 'attack':274C,356C 'behaviour':514C 'beyond':359C 'browser':223C,388C,478C,631C 'build':458C,523C 'bypass':698C 'call':456C 'case':193C 'caus':439C 'chang':305C,372C,619C 'claim':170C,261C,366C,414C,428C,484C,498C,547C 'common':192C 'compon':440C 'concept':561C 'configur':381C 'contain':254C 'control':677C 'correct':220C 'credit':690C 'current':509C 'data':490C,526C 'default':237C,244C,250C,380C 'deploy':306C 'document':513C 'either':308C 'empti':364C,546C 'enabl':232C,239C,583C,588C 'endpoint':121C,445C 'entra':186C 'everyth':350C 'exchang':107C,120C,195C,235C,340C,408C,417C,437C,444C,471C,516C,586C,701C 'exist':204C,554C 'exploit':375C 'fals':238C,245C 'fell':549C 'fix':400C,401C 'found':172C 'gain':357C 'genuin':503C 'get':227C,670C 'grant':369C 'held':346C 'hold':606C,681C 'id':114C,183C,187C,265C,482C,520C,539C,581C,710C 'ident':175C,256C,285C 'impact':321C 'instead':556C 'intend':399C 'keep':102C,200C,335C,342C,507C,552C 'let':122C 'link':129C,281C,614C 'list':598C,629C 'local':572C 'lock':328C 'login':224C,390C,479C,633C 'longer':213C 'manag':242C,422C,591C 'manus':692C 'manus-pi':691C 'mean':500C 'meant':326C 'merg':486C 'microsoft':185C 'need':376C 'neither':368C 'never':517C 'non':379C 'non-default':378C 'none':505C 'noth':358C 'oauth':118C,130C,147C,233C,240C,247C,332C,442C,452C,584C,589C,595C 'obtain':289C,641C 'oidc':573C 'one':625C 'open':139C,278C 'oper':325C 'outsid':626C 'packag':569C 'pi':693C 'polici':149C,216C,334C,454C,468C,474C,493C,542C,697C 'post':446C,650C 'precondit':231C 'previous':361C,385C 'proof':559C 'provid':134C,157C,176C,210C,257C,284C,296C,298C,313C,448C,502C,532C,574C,618C,647C,652C,703C 'provis':386C 'put':258C,535C,576C 'pypi':568C 'reach':354C 'read':153C,411C,431C 'receiv':518C 'refus':164C,221C,395C,435C,558C,634C 'releas':404C,463C 'report':694C 'reproduc':562C 'request':660C 'respons':160C,272C 'return':314C,661C,675C 'revok':100C 'role':109C,148C,154C,169C,179C,189C,198C,205C,211C,241C,249C,260C,304C,315C,333C,344C,352C,365C,374C,413C,421C,427C,453C,467C,473C,497C,510C,536C,555C,577C,590C,597C,609C,623C,684C,696C,706C 'root':438C 'run':494C 'satisfi':214C 'saw':544C 'see':196C 'send':178C,649C,705C 'sent':504C 'session':141C,230C,665C 'set':309C 'sign':103C,336C,600C 'sinc':142C 'sso':389C,604C,632C 'still':226C,288C,680C 'summari':116C 'swap':132C 'togeth':348C 'token':106C,115C,119C,136C,184C,194C,234C,266C,293C,339C,407C,418C,443C,470C,483C,515C,521C,540C,582C,585C,644C,655C,666C,700C,711C 'true':236C,243C,587C,592C 'user':101C,124C,166C,202C,208C,251C,323C,397C,489C,525C,621C,679C 'userinfo':159C,271C,317C,528C 'valid':291C 'version':639C,689C 'via':105C 'webui':140C,279C 'whose':209C,312C 'work':229C,668C 'written':476C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-09-27T16:14:42+00:00","kev":null,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","cvss_by":"disclosure@vulncheck.com","poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-2rr4-q6pg-m5g3","url":"https://github.com/open-webui/open-webui/security/advisories/GHSA-2rr4-q6pg-m5g3","severity":"medium","published_at":"2026-09-27T16:14:42+00:00","modified_at":"2026-10-28T20:46:49+00:00","first_seen_at":"2026-11-09T22:53:28+00:00","fetched_at":"2026-11-09T22:53:28+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-109283","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-109283","severity":null,"published_at":"2026-10-12T17:17:21+00:00","modified_at":"2026-10-13T20:39:48+00:00","first_seen_at":"2026-11-09T23:10:12+00:00","fetched_at":"2026-11-09T23:10:12+00:00","note":null}],"documents":[{"id":110659,"url":"https://github.com/open-webui/open-webui/security/advisories/GHSA-2rr4-q6pg-m5g3","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":"2026-09-27T16:14:42+00:00","relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":111014,"url":"https://www.vulncheck.com/advisories/open-webui-0.11.1-before-0.11.4-authorization-bypass-via-oauth-token-exchange","title":"Third-party advisory: www.vulncheck.com","kind":"advisory","source":"nvd","publisher":"research","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Third-party advisory: www.vulncheck.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"}