Repository navigation
mod_autht_jwt: Fix authentication bypass via claim-name prefix matching (BZ 69946) - #950
Open
arturobernalg wants to merge 1 commit into
Open
arturobernalg wants to merge 1 commit into
arturobernalg wants to merge 1 commit into
Conversation
arturobernalg
added a commit
to arturobernalg/httpd
that referenced
this pull request
Nov 5, 2026
…ng (BZ 69946)
check_token() identified the 'aud', 'sub', 'exp' and 'nbf' claims with:
strncmp("aud", kv->k->value.string.p, kv->k->value.string.len)
strncmp(a, b, n) only compares the first n bytes, and n here is the
actual claim-name length, not the length of the literal. A claim
whose name is a proper prefix of "aud" (e.g. "a" or "au") therefore
satisfies this check and is treated as the real 'aud' claim. A JWT
that omits 'aud' entirely but carries such a short claim set to the
expected AuthName value then passes the audience check that exists
specifically to confirm the token was issued for this location --
an authentication bypass. The same pattern affected 'sub', 'exp'
and 'nbf'.
Fix: require the claim-name length to match the literal's length
before comparing, so only an exact match counts.
Confirmed with a standalone C harness mirroring the exact comparison
(strncmp("aud", "au", 2) == 0 before the fix; correctly rejected
after). Confirmed via direct code reading that the bug has been
present since the module's creation in d8246a1 (April 2023) and
no later commit touches these lines.
Verified against a real, running httpd + mod_autht_jwt (built apr-util
1.7 from source with OpenSSL crypto support, since the module needs
apr_jose which requires apr-util >= 1.7). With the fix applied, a JWT
with no 'aud' claim but an 'au' claim set to the AuthName value (the
bypass) is rejected with the same AH10444 'aud value was missing'
message as a token with no aud-like claim at all; same result for a
single-character 'a' claim.
Added a pytest regression test (test_mod_autht_jwt.py) building real
HS256 JWTs by hand. Two test-infrastructure bugs found and fixed along
the way, independent of the production fix above:
- AuthtJwtVerify reads its secret file while parsing the config, before
httpd starts; writing it from inside each test function (as the first
version of this test did) runs too late. Fixed by writing it once,
centrally, the same way TestConfig already generates the SSL CA
(apache_pytest/config.py).
- modules/aaa/mod_autht_jwt.c can be present as a loadable module
(satisfying <IfModule>) while still being a non-functional stub that
registers neither AuthtJwtVerify nor the 'jwt' Autht provider, on any
build whose apr-util lacks apr_jose (a separate, pre-existing bug,
BZ 70276/70277). Without a guard, the new autht_jwt.conf.in fragment
broke httpd startup -- and the entire shared pytest_suite session, not
just these four tests -- on exactly such a CI leg (confirmed via the
actual failing GitHub Actions log for PR apache#950, both 'Python
pytest_suite, motorz MPM' and the 64-bit ARM job). Fixed by also
guarding on <IfDirective AuthtJwtVerify>, which I verified directly
with httpd -t against both a stub build (skips the block, Syntax OK)
and a real apr-util-1.7-linked build (still applies the block).
Claude-Session: https://claude.ai/code/session_01BrsHTcwUro4KV5Fp5hv8XM
arturobernalg
force-pushed
the
fix/bz69946-jwt-claim-prefix-match
branch
from
November 5, 2026 06:43
2671cf8 to
0729b86
Compare
arturobernalg
added a commit
to arturobernalg/httpd
that referenced
this pull request
Nov 5, 2026
…ng (BZ 69946)
check_token() identified the 'aud', 'sub', 'exp' and 'nbf' claims with:
strncmp("aud", kv->k->value.string.p, kv->k->value.string.len)
strncmp(a, b, n) only compares the first n bytes, and n here is the
actual claim-name length, not the length of the literal. A claim
whose name is a proper prefix of "aud" (e.g. "a" or "au") therefore
satisfies this check and is treated as the real 'aud' claim. A JWT
that omits 'aud' entirely but carries such a short claim set to the
expected AuthName value then passes the audience check that exists
specifically to confirm the token was issued for this location --
an authentication bypass. The same pattern affected 'sub', 'exp'
and 'nbf'.
Fix: require the claim-name length to match the literal's length
before comparing, so only an exact match counts.
Confirmed with a standalone C harness mirroring the exact comparison
(strncmp("aud", "au", 2) == 0 before the fix; correctly rejected
after). Confirmed via direct code reading that the bug has been
present since the module's creation in d8246a1 (April 2023) and
no later commit touches these lines.
Verified against a real, running httpd + mod_autht_jwt (built apr-util
1.7 from source with OpenSSL crypto support, since the module needs
apr_jose which requires apr-util >= 1.7). With the fix applied, a JWT
with no 'aud' claim but an 'au' claim set to the AuthName value (the
bypass) is rejected with the same AH10444 'aud value was missing'
message as a token with no aud-like claim at all; same result for a
single-character 'a' claim.
Added a pytest regression test (test_mod_autht_jwt.py) building real
HS256 JWTs by hand. Two test-infrastructure bugs found and fixed along
the way, independent of the production fix above:
- AuthtJwtVerify reads its secret file while parsing the config, before
httpd starts; writing it from inside each test function (as the first
version of this test did) runs too late. Fixed by writing it once,
centrally, the same way TestConfig already generates the SSL CA
(apache_pytest/config.py).
- modules/aaa/mod_autht_jwt.c can be present as a loadable module
(satisfying <IfModule>) while still being a non-functional stub that
registers neither AuthtJwtVerify nor the 'jwt' Autht provider, on any
build whose apr-util lacks apr_jose (a separate, pre-existing bug,
BZ 70276/70277). Without a guard, the new autht_jwt.conf.in fragment
broke httpd startup -- and the entire shared pytest_suite session, not
just these four tests -- on exactly such a CI leg (confirmed via the
actual failing GitHub Actions log for PR apache#950, both 'Python
pytest_suite, motorz MPM' and the 64-bit ARM job). Fixed by also
guarding on <IfDirective AuthtJwtVerify>, which I verified directly
with httpd -t against both a stub build (skips the block, Syntax OK)
and a real apr-util-1.7-linked build (still applies the block).
Claude-Session: https://claude.ai/code/session_01BrsHTcwUro4KV5Fp5hv8XM
arturobernalg
force-pushed
the
fix/bz69946-jwt-claim-prefix-match
branch
from
November 5, 2026 07:45
0729b86 to
f38954f
Compare
…ng (BZ 69946)
check_token() identified the 'aud', 'sub', 'exp' and 'nbf' claims with:
strncmp("aud", kv->k->value.string.p, kv->k->value.string.len)
strncmp(a, b, n) only compares the first n bytes, and n here is the
actual claim-name length, not the length of the literal. A claim
whose name is a proper prefix of "aud" (e.g. "a" or "au") therefore
satisfies this check and is treated as the real 'aud' claim. A JWT
that omits 'aud' entirely but carries such a short claim set to the
expected AuthName value then passes the audience check that exists
specifically to confirm the token was issued for this location --
an authentication bypass. The same pattern affected 'sub', 'exp'
and 'nbf'.
Fix: require the claim-name length to match the literal's length
before comparing, so only an exact match counts.
Confirmed with a standalone C harness mirroring the exact comparison
(strncmp("aud", "au", 2) == 0 before the fix; correctly rejected
after). Confirmed via direct code reading that the bug has been
present since the module's creation in d8246a1 (April 2023) and
no later commit touches these lines.
Verified against a real, running httpd + mod_autht_jwt (built apr-util
1.7 from source with OpenSSL crypto support, since the module needs
apr_jose which requires apr-util >= 1.7). With the fix applied, a JWT
with no 'aud' claim but an 'au' claim set to the AuthName value (the
bypass) is rejected with the same AH10444 'aud value was missing'
message as a token with no aud-like claim at all; same result for a
single-character 'a' claim.
Added a pytest regression test (test_mod_autht_jwt.py) building real
HS256 JWTs by hand. Two test-infrastructure bugs found and fixed along
the way, independent of the production fix above:
- AuthtJwtVerify reads its secret file while parsing the config, before
httpd starts; writing it from inside each test function (as the first
version of this test did) runs too late. Fixed by writing it once,
centrally, the same way TestConfig already generates the SSL CA
(apache_pytest/config.py).
- modules/aaa/mod_autht_jwt.c can be present as a loadable module
(satisfying <IfModule>) while still being a non-functional stub that
registers neither AuthtJwtVerify nor the 'jwt' Autht provider, on any
build whose apr-util lacks apr_jose (a separate, pre-existing bug,
BZ 70276/70277). Without a guard, the new autht_jwt.conf.in fragment
broke httpd startup -- and the entire shared pytest_suite session, not
just these four tests -- on exactly such a CI leg (confirmed via the
actual failing GitHub Actions log for PR apache#950, both 'Python
pytest_suite, motorz MPM' and the 64-bit ARM job). Fixed by also
guarding on <IfDirective AuthtJwtVerify>, which I verified directly
with httpd -t against both a stub build (skips the block, Syntax OK)
and a real apr-util-1.7-linked build (still applies the block).
Claude-Session: https://claude.ai/code/session_01BrsHTcwUro4KV5Fp5hv8XM
arturobernalg
force-pushed
the
fix/bz69946-jwt-claim-prefix-match
branch
from
November 5, 2026 08:36
f38954f to
aae688d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
check_token() identified the
aud,sub,expandnbfclaims with:strncmp(a, b, n) only compares the first n bytes, and n here is the actual claim-name length, not the length of the literal. A claim whose name is a proper prefix of "aud" (e.g. "a" or "au") therefore satisfies this check and is treated as the real 'aud' claim. A JWT that omits 'aud' entirely but carries such a short claim set to the expected AuthName value then passes the audience check that exists specifically to confirm the token was issued for this location -- an authentication bypass. The same pattern affected 'sub', 'exp' and 'nbf'.
Fix: require the claim-name length to match the literal's length before comparing, so only an exact match counts.
Bugzilla: https://bz.apache.org/bugzilla/show_bug.cgi?id=69946 (filed 2026-02-04, public, no existing patch or PR)
Verified against a real, running httpd + mod_autht_jwt (not just static analysis): built apr-util 1.7 from source (github.com/apache/apr-util, with OpenSSL crypto support) since this environment's system apr-util was too old for the module, then built and ran httpd with AuthType bearer / AuthBearerProvider jwt against it. With the fix applied:
audclaim but aauclaim set to the AuthName value (the bypass) is correctly rejected: log shows "AH10444: ... 'aud' value was missing and did not match AuthName", identical to the case whereaudis absent entirely.aclaim.Added a pytest regression test (test_mod_autht_jwt.py) building real HS256 JWTs by hand. I could not run the full pytest suite in my sandbox (system apr-util there is 1.6.5, below the 1.7 the module requires, so it compiles to a no-op stub there), but I did verify the underlying behavior manually against a real, apr-util-1.7-linked build as described above. Please confirm the pytest file itself passes in CI before merge.
(see SECURITY.md and
https://www.apache.org/security/#reporting-a-vulnerability).
This fixes BZ 69946, already public (filed 2026-02-04, listed on the
public bugs@ mailing list and the public bug-health report) for ~9
months with no patch; not a new disclosure.
APLOGNO()tag;numbers are assigned by a committer at merge time (see
docs/log-message-tags/README).N/A -- no new log messages in this change.
changes-entries/*.txtfile is included,following the template in
README.CHANGES(not needed otherwise).test/modules/xxxdirectory matching themodules/xxxmodule source,or
test/modules/corefor core server changes.https://claude.ai/code/session_01BrsHTcwUro4KV5Fp5hv8XM