{"repro_id":"REPRO-2026-00281","version":6,"title":"Apache Kafka SASL/OAUTHBEARER accepts unvalidated JWTs","repro_type":"security","status":"published","severity":"critical","description":"Suspected authentication bypass in Apache Kafka broker-side SASL/OAUTHBEARER token acceptance path. The broker appears to accept historical JWT session tokens indefinitely because the unauthenticated token acceptance path does not enforce the JWT exp claim on replay. An attacker holding any previously issued Kafka session JWT can replay it after expiration and still authenticate successfully. This is distinct from generic token validation issues because the claim failure is specifically in exp enforcement on the broker side. Investigate broker-side SASL/OAUTHBEARER handling for expired-but-signed tokens, the exact validation path used during initial connection establishment, and whether the default validator or an alternate callback path bypasses exp checking. Scope: latest Apache Kafka release line unless a patched release already covers this behavior.","root_cause":"## Summary\n\nApache Kafka 4.1.0/4.1.1 broker-side SASL/OAUTHBEARER validation can accept attacker-supplied JWTs through the network authentication path when the default broker validator is used. The current reproduction focuses on the judge-requested observable vulnerable-vs-fixed divergence for an expired JWT replayed directly over Kafka's TCP SASL boundary: Kafka 4.1.0 returns a successful `SaslAuthenticate` response for the expired replayed JWT, while Kafka 4.1.2 rejects the same token with `invalid_token` and broker-side `BrokerJwtValidator` expiration evidence. This avoids Kafka client-side expiry enforcement by using a raw Kafka protocol client only for the SASL frames.\n\n## Impact\n\n- Package/component affected: Apache Kafka broker SASL/OAUTHBEARER authentication, specifically `org.apache.kafka.common.security.oauthbearer.DefaultJwtValidator` used by `OAuthBearerValidatorCallbackHandler`.\n- Affected versions: Apache Kafka 4.1.0 through 4.1.1 according to the Apache advisory for CVE-2026-33557.\n- Fixed versions: Apache Kafka 4.1.2 and 4.2.0+.\n- Risk level and consequences: Important/high authentication validation weakness. The advisory impact is that a broker using the default validator may accept JWTs without validating signature, issuer, or audience. In this run, the expired-token replay path demonstrates that the vulnerable broker accepts an expired JWT at the SASL authentication step, while the fixed broker rejects it.\n\n## Impact Parity\n\n- Disclosed/claimed maximum impact: `authz_bypass` / authentication validation bypass for Kafka SASL/OAUTHBEARER.\n- Reproduced impact from this run: network-protocol authentication divergence on the real Kafka broker TCP listener. The vulnerable broker returns `SaslAuthenticate(error_code=0)` for an expired JWT replayed directly in the SASL initial client response; the fixed broker returns an invalid-token challenge and logs expiration validation failure.\n- Parity: `full` for the broker-side token validation bypass at the SASL/OAUTHBEARER TCP authentication boundary.\n- Not demonstrated: The proof does not require a Kafka CLI client to use the expired credential because the standard client enforces expiry before replay. The primary proof intentionally bypasses that client-side behavior with raw Kafka SASL frames to test the broker-side validator directly.\n\n## Root Cause\n\nKafka 4.1.0's `DefaultJwtValidator.configure()` always delegates to `ClientJwtValidator` when no explicit verification key resolver is passed. `ClientJwtValidator` performs only structural and claim-presence checks for `scope`, `exp`, `sub`, and `iat`; it does not verify the JWT signature and does not reject an `exp` timestamp that is already in the past. Therefore, the broker-side `OAuthBearerValidatorCallbackHandler` can receive a replayed JWT over TCP and treat it as valid.\n\nKafka 4.1.2 changes `DefaultJwtValidator.configure()` so that when `sasl.oauthbearer.jwks.endpoint.url` is present, the default delegate becomes `BrokerJwtValidator`, which performs signature and claims validation, including expiration. The fix commit referenced by the advisory is `01d8e7db8d08dbd538892b409457ea6bfcc2a422`.\n\n## Reproduction Steps\n\n1. Run `bundle/repro/reproduction_steps.sh`.\n2. The script installs required Python dependencies, reuses or downloads Kafka 4.1.0 and Kafka 4.1.2 binaries, starts a real mock JWKS/OAuth issuer, starts real Kafka KRaft brokers with SASL/OAUTHBEARER enabled, and sends raw Kafka `SaslHandshake` and `SaslAuthenticate` TCP frames containing the same expired attacker-controlled JWT.\n3. Expected evidence of reproduction:\n   - Kafka 4.1.0: expired JWT replay receives `SaslAuthenticate` success (`error_code=0`, no invalid-token auth bytes).\n   - Kafka 4.1.2: the same expired JWT receives an invalid-token response and broker logs show `BrokerJwtValidator` rejecting the `exp` claim as expired.\n   - Valid JWT controls are also run so the fixed broker's rejection is not caused by a broken test setup.\n\n## Evidence\n\nPrimary evidence is written under `bundle/logs/`:\n\n- `bundle/logs/evidence.log`: summary, decoded JWT claims, version comparison, and key protocol outcomes.\n- `bundle/logs/raw_vulnerable_expired.json`: parsed raw Kafka SASL response showing vulnerable acceptance of the expired JWT.\n- `bundle/logs/raw_fixed_expired.json`: parsed raw Kafka SASL response showing fixed rejection of the same expired JWT.\n- `bundle/logs/broker_vulnerable.log`: real Kafka 4.1.0 broker runtime log.\n- `bundle/logs/broker_fixed.log`: real Kafka 4.1.2 broker runtime log with expiration rejection details.\n- `bundle/logs/mock_oauth_server.log`: generated expired/valid JWTs and JWKS requests.\n\nKey expected excerpts:\n\n- Vulnerable parsed response: `\"sasl_authenticate_error_code\": 0`, `\"accepted_by_broker\": true`, and an expired JWT claim where `exp < replay_time`.\n- Fixed parsed response: `\"rejected_by_broker\": true` with `\"invalid_token\"` in the auth bytes/error payload.\n- Fixed broker log: `The JWT is no longer valid ... Expiration Time ...` from `BrokerJwtValidator`.\n\nEnvironment details captured include Java version, Kafka binary versions, mock issuer/JWKS URL, decoded token timestamps, and the raw TCP proof JSON files.\n\n## Recommendations / Next Steps\n\n- Upgrade affected Kafka deployments to 4.1.2, 4.2.0, or later.\n- If upgrade is not immediately possible, explicitly configure `sasl.oauthbearer.jwt.validator.class=org.apache.kafka.common.security.oauthbearer.BrokerJwtValidator` for brokers using SASL/OAUTHBEARER and configure a trusted `sasl.oauthbearer.jwks.endpoint.url`.\n- Add integration tests that replay expired, wrong-issuer, wrong-audience, and unsigned/tampered JWTs directly against the broker-side SASL/OAUTHBEARER listener, not only through Kafka clients.\n\n## Additional Notes\n\nThe reproduction script is designed to be idempotent: it creates isolated per-run KRaft data directories, uses unique ports per attempt where practical, kills broker/mock processes on exit, and writes fresh runtime evidence on every run. The proof intentionally uses a raw Kafka TCP client after broker startup because the standard Kafka CLI client refuses to replay expired credentials locally; using it would mask the broker-side validator path that this ticket asks to verify.\n","cve_id":"CVE-2026-33557","source_url":"https://github.com/apache/kafka","package":{"name":"Apache Kafka","ecosystem":"maven","affected_versions":"4.1.0 through 4.1.1"},"reproduced_at":"2026-07-11T12:55:55.088067+00:00","duration_secs":2665.0,"tool_calls":414,"handoffs":3,"total_cost_usd":10.594407620000002,"agent_costs":{"judge":0.1045339,"repro":7.853035610000002,"support":0.15661611,"vuln_variant":2.480222},"cost_breakdown":{"judge":{"gpt-5.4-mini":0.1045339},"repro":{"accounts/fireworks/routers/glm-5p2-fast":4.80953961,"gpt-5.5":3.043495999999999},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.15661611},"vuln_variant":{"gpt-5.5":2.480222}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-11T12:56:30.639928+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":26869,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":6267,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":28942,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":12730,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":12629,"category":"other"},{"path":"bundle/artifact_promotion_report.json","filename":"artifact_promotion_report.json","size":12647,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":866,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1419,"category":"other"},{"path":"bundle/logs/evidence.log","filename":"evidence.log","size":7908,"category":"log"},{"path":"bundle/logs/broker_secured.log","filename":"broker_secured.log","size":85540,"category":"log"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1049,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":829,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":7490,"category":"log"},{"path":"bundle/logs/raw_vulnerable_expired.json","filename":"raw_vulnerable_expired.json","size":884,"category":"other"},{"path":"bundle/logs/raw_vulnerable_valid.json","filename":"raw_vulnerable_valid.json","size":889,"category":"other"},{"path":"bundle/logs/raw_fixed_expired.json","filename":"raw_fixed_expired.json","size":1301,"category":"other"},{"path":"bundle/logs/raw_fixed_valid.json","filename":"raw_fixed_valid.json","size":884,"category":"other"},{"path":"bundle/logs/broker_vulnerable.log","filename":"broker_vulnerable.log","size":73143,"category":"log"},{"path":"bundle/logs/broker_fixed.log","filename":"broker_fixed.log","size":77246,"category":"log"},{"path":"bundle/logs/mock_oauth_server.log","filename":"mock_oauth_server.log","size":5742,"category":"log"},{"path":"bundle/logs/javap_defaultjwtvalidator_4.1.0.txt","filename":"javap_defaultjwtvalidator_4.1.0.txt","size":5016,"category":"other"},{"path":"bundle/logs/javap_defaultjwtvalidator_4.1.2.txt","filename":"javap_defaultjwtvalidator_4.1.2.txt","size":6150,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":9888,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":4750,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":2207,"category":"other"},{"path":"bundle/logs/vuln_variant/variant_evidence.log","filename":"variant_evidence.log","size":17321,"category":"log"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1217,"category":"other"},{"path":"bundle/logs/vuln_variant/reproduction_steps.log","filename":"reproduction_steps.log","size":12942,"category":"log"},{"path":"bundle/logs/vuln_variant/fixed_version.txt","filename":"fixed_version.txt","size":420,"category":"other"},{"path":"bundle/logs/vuln_variant/raw_vulnerable_wrong_issuer.json","filename":"raw_vulnerable_wrong_issuer.json","size":898,"category":"other"},{"path":"bundle/logs/vuln_variant/raw_vulnerable_wrong_audience.json","filename":"raw_vulnerable_wrong_audience.json","size":897,"category":"other"},{"path":"bundle/logs/vuln_variant/raw_vulnerable_tampered_signature.json","filename":"raw_vulnerable_tampered_signature.json","size":900,"category":"other"},{"path":"bundle/logs/vuln_variant/raw_fixed_wrong_issuer.json","filename":"raw_fixed_wrong_issuer.json","size":1309,"category":"other"},{"path":"bundle/logs/vuln_variant/raw_fixed_wrong_audience.json","filename":"raw_fixed_wrong_audience.json","size":1308,"category":"other"},{"path":"bundle/logs/vuln_variant/raw_fixed_tampered_signature.json","filename":"raw_fixed_tampered_signature.json","size":1311,"category":"other"},{"path":"bundle/logs/vuln_variant/broker_vulnerable.log","filename":"broker_vulnerable.log","size":73290,"category":"log"},{"path":"bundle/logs/vuln_variant/broker_fixed.log","filename":"broker_fixed.log","size":85013,"category":"log"},{"path":"bundle/logs/vuln_variant/mock_oauth_server.log","filename":"mock_oauth_server.log","size":4943,"category":"log"}]}