# REPRO-2026-00281: Apache Kafka SASL/OAUTHBEARER accepts unvalidated JWTs ## Summary Status: published Severity: critical Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00281 CVE: CVE-2026-33557 ## Package Name: Apache Kafka Ecosystem: maven Affected: 4.1.0 through 4.1.1 Fixed: Unknown ## Root Cause ## Summary Apache 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. ## Impact - Package/component affected: Apache Kafka broker SASL/OAUTHBEARER authentication, specifically `org.apache.kafka.common.security.oauthbearer.DefaultJwtValidator` used by `OAuthBearerValidatorCallbackHandler`. - Affected versions: Apache Kafka 4.1.0 through 4.1.1 according to the Apache advisory for CVE-2026-33557. - Fixed versions: Apache Kafka 4.1.2 and 4.2.0+. - 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. ## Impact Parity - Disclosed/claimed maximum impact: `authz_bypass` / authentication validation bypass for Kafka SASL/OAUTHBEARER. - 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. - Parity: `full` for the broker-side token validation bypass at the SASL/OAUTHBEARER TCP authentication boundary. - 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. ## Root Cause Kafka 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. Kafka 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`. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh`. 2. 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. 3. Expected evidence of reproduction: - Kafka 4.1.0: expired JWT replay receives `SaslAuthenticate` success (`error_code=0`, no invalid-token auth bytes). - Kafka 4.1.2: the same expired JWT receives an invalid-token response and broker logs show `BrokerJwtValidator` rejecting the `exp` claim as expired. - Valid JWT controls are also run so the fixed broker's rejection is not caused by a broken test setup. ## Evidence Primary evidence is written under `bundle/logs/`: - `bundle/logs/evidence.log`: summary, decoded JWT claims, version comparison, and key protocol outcomes. - `bundle/logs/raw_vulnerable_expired.json`: parsed raw Kafka SASL response showing vulnerable acceptance of the expired JWT. - `bundle/logs/raw_fixed_expired.json`: parsed raw Kafka SASL response showing fixed rejection of the same expired JWT. - `bundle/logs/broker_vulnerable.log`: real Kafka 4.1.0 broker runtime log. - `bundle/logs/broker_fixed.log`: real Kafka 4.1.2 broker runtime log with expiration rejection details. - `bundle/logs/mock_oauth_server.log`: generated expired/valid JWTs and JWKS requests. Key expected excerpts: - Vulnerable parsed response: `"sasl_authenticate_error_code": 0`, `"accepted_by_broker": true`, and an expired JWT claim where `exp < replay_time`. - Fixed parsed response: `"rejected_by_broker": true` with `"invalid_token"` in the auth bytes/error payload. - Fixed broker log: `The JWT is no longer valid ... Expiration Time ...` from `BrokerJwtValidator`. Environment details captured include Java version, Kafka binary versions, mock issuer/JWKS URL, decoded token timestamps, and the raw TCP proof JSON files. ## Recommendations / Next Steps - Upgrade affected Kafka deployments to 4.1.2, 4.2.0, or later. - 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`. - 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. ## Additional Notes The 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. ## Reproduction Details Reproduced: 2026-07-11T12:55:55.088Z Duration: 2665 seconds Tool calls: 414 Turns: Unknown Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00281 pruva-verify CVE-2026-33557 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00281&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00281/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-33557 - Source: https://github.com/apache/kafka ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 26869 bytes) - bundle/repro/rca_report.md (analysis, 6267 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 28942 bytes) - bundle/vuln_variant/rca_report.md (analysis, 12730 bytes) - bundle/artifact_promotion_manifest.json (other, 12629 bytes) - bundle/artifact_promotion_report.json (other, 12647 bytes) - bundle/vuln_variant/source_identity.json (other, 866 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 1419 bytes) - bundle/logs/evidence.log (log, 7908 bytes) - bundle/logs/broker_secured.log (log, 85540 bytes) - bundle/repro/runtime_manifest.json (other, 1049 bytes) - bundle/repro/validation_verdict.json (other, 829 bytes) - bundle/logs/reproduction_steps.log (log, 7490 bytes) - bundle/logs/raw_vulnerable_expired.json (other, 884 bytes) - bundle/logs/raw_vulnerable_valid.json (other, 889 bytes) - bundle/logs/raw_fixed_expired.json (other, 1301 bytes) - bundle/logs/raw_fixed_valid.json (other, 884 bytes) - bundle/logs/broker_vulnerable.log (log, 73143 bytes) - bundle/logs/broker_fixed.log (log, 77246 bytes) - bundle/logs/mock_oauth_server.log (log, 5742 bytes) - bundle/logs/javap_defaultjwtvalidator_4.1.0.txt (other, 5016 bytes) - bundle/logs/javap_defaultjwtvalidator_4.1.2.txt (other, 6150 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 9888 bytes) - bundle/vuln_variant/variant_manifest.json (other, 4750 bytes) - bundle/vuln_variant/validation_verdict.json (other, 2207 bytes) - bundle/logs/vuln_variant/variant_evidence.log (log, 17321 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1217 bytes) - bundle/logs/vuln_variant/reproduction_steps.log (log, 12942 bytes) - bundle/logs/vuln_variant/fixed_version.txt (other, 420 bytes) - bundle/logs/vuln_variant/raw_vulnerable_wrong_issuer.json (other, 898 bytes) - bundle/logs/vuln_variant/raw_vulnerable_wrong_audience.json (other, 897 bytes) - bundle/logs/vuln_variant/raw_vulnerable_tampered_signature.json (other, 900 bytes) - bundle/logs/vuln_variant/raw_fixed_wrong_issuer.json (other, 1309 bytes) - bundle/logs/vuln_variant/raw_fixed_wrong_audience.json (other, 1308 bytes) - bundle/logs/vuln_variant/raw_fixed_tampered_signature.json (other, 1311 bytes) - bundle/logs/vuln_variant/broker_vulnerable.log (log, 73290 bytes) - bundle/logs/vuln_variant/broker_fixed.log (log, 85013 bytes) - bundle/logs/vuln_variant/mock_oauth_server.log (log, 4943 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00281 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00281/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00281 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev