High severity7.3NVD Advisory· Published Apr 22, 2026· Updated May 1, 2026
CVE-2026-40542
CVE-2026-40542
Description
Missing critical step in authentication in Apache HttpClient 5.6 allows an attacker to cause the client to accept SCRAM-SHA-256 authentication without proper mutual authentication verification. Users are recommended to upgrade to version 5.6.1, which fixes this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.httpcomponents.client5:httpclient5Maven | >= 5.6-alpha1, < 5.6.1 | 5.6.1 |
Affected products
1- cpe:2.3:a:apache:httpclient:5.6:-:*:*:*:*:*:*
Patches
1726eac2323d3Fix SCRAM final response handling
6 files changed · +441 −74
httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java+13 −0 modified@@ -227,6 +227,19 @@ public boolean handleResponse( } final Map<String, AuthChallenge> challengeMap = extractChallengeMap(challengeType, response, clientContext); + if (challengeMap.isEmpty() && !challenged && isChallengeExpected) { + final AuthScheme authScheme = authExchange.getAuthScheme(); + if (authScheme != null) { + MessageSupport.parseHeaders( + response, + challengeType == ChallengeType.PROXY ? "Proxy-Authentication-Info" : "Authentication-Info", + (buffer, cursor) -> { + final String schemeName = authScheme.getName(); + final AuthChallenge authChallenge = parser.parse(challengeType, schemeName, buffer, cursor); + challengeMap.put(schemeName.toLowerCase(Locale.ROOT), authChallenge); + }); + } + } if (challengeMap.isEmpty()) { if (LOG.isDebugEnabled()) {
httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/ScramScheme.java+100 −15 modified@@ -80,6 +80,9 @@ public final class ScramScheme implements AuthScheme { private static final Logger LOG = LoggerFactory.getLogger(ScramScheme.class); + private static final int DEFAULT_WARN_MIN_ITERATIONS = 4096; + private static final int DEFAULT_MAX_ITERATIONS_ALLOWED = 100000; + // RFC 7804 / RFC 5802 fixed no-CB GS2 header and its base64 value for 'c=' private static final String GS2_HEADER = "n,,"; private static final String C_BIND_B64 = "biws"; // base64("n,,") @@ -100,6 +103,7 @@ private enum State { private final SecureRandom secureRandom; private final int warnMinIterations; private final int minIterationsRequired; + private final int maxIterationsAllowed; private State state = State.INIT; private boolean complete; @@ -128,7 +132,7 @@ private enum State { * @since 5.6 */ public ScramScheme() { - this(4096, 0, null); + this(DEFAULT_WARN_MIN_ITERATIONS, 0, DEFAULT_MAX_ITERATIONS_ALLOWED, null); } /** @@ -140,8 +144,26 @@ public ScramScheme() { * @since 5.6 */ public ScramScheme(final int warnMinIterations, final int minIterationsRequired, final SecureRandom rnd) { + this(warnMinIterations, minIterationsRequired, DEFAULT_MAX_ITERATIONS_ALLOWED, rnd); + } + + /** + * Constructor with custom iteration policy. + * + * @param warnMinIterations warn if iteration count is lower than this (0 disables warnings) + * @param minIterationsRequired fail if iteration count is lower than this (0 disables enforcement) + * @param maxIterationsAllowed fail if iteration count is greater than this (must be positive) + * @param rnd optional secure random source (null uses system default) + * @since 5.6 + */ + public ScramScheme( + final int warnMinIterations, + final int minIterationsRequired, + final int maxIterationsAllowed, + final SecureRandom rnd) { this.warnMinIterations = Math.max(0, warnMinIterations); this.minIterationsRequired = Math.max(0, minIterationsRequired); + this.maxIterationsAllowed = Args.positive(maxIterationsAllowed, "Max iterations allowed"); this.secureRandom = rnd != null ? rnd : new SecureRandom(); } @@ -206,9 +228,10 @@ public void processChallenge( Args.notNull(context, "HTTP context"); if (authChallenge == null) { - if (!challenged) { - // Final response with no Authentication-Info: nothing to do - return; + if (!challenged && this.state == State.CLIENT_FINAL_SENT) { + zeroAndClearExpectedV(); + this.state = State.FAILED; + throw new AuthenticationException("Missing SCRAM Authentication-Info"); } throw new MalformedChallengeException("Null SCRAM challenge"); } @@ -232,10 +255,32 @@ public void processChallenge( return; } + final String sid = params.get("sid"); + if (sid == null || sid.isEmpty()) { + zeroAndClearExpectedV(); + this.state = State.FAILED; + throw new MalformedChallengeException("SCRAM server-first missing sid"); + } + // server-first (data present) - final String decoded = b64ToString(data); + final String decoded; + try { + decoded = b64ToString(data); + } catch (final MalformedChallengeException ex) { + zeroAndClearExpectedV(); + this.state = State.FAILED; + throw ex; + } this.serverFirstRaw = decoded; - final Map<String, String> attrs = parseAttrs(decoded); + + final Map<String, String> attrs; + try { + attrs = parseAttrs(decoded); + } catch (final MalformedChallengeException ex) { + zeroAndClearExpectedV(); + this.state = State.FAILED; + throw ex; + } final String r = attrs.get("r"); final String s = attrs.get("s"); @@ -249,7 +294,6 @@ public void processChallenge( throw new AuthenticationException("SCRAM server nonce does not start with client nonce"); } - this.sid = params.get("sid"); try { this.salt = B64D.decode(s); if (this.salt.length == 0) { @@ -274,25 +318,66 @@ public void processChallenge( throw new AuthenticationException( "SCRAM iteration count below required minimum: " + this.iterations + " < " + this.minIterationsRequired); } + if (this.iterations > this.maxIterationsAllowed) { + this.state = State.FAILED; + throw new AuthenticationException( + "SCRAM iteration count above allowed maximum: " + this.iterations + " > " + this.maxIterationsAllowed); + } if (this.warnMinIterations > 0 && this.iterations < this.warnMinIterations && LOG.isWarnEnabled()) { LOG.warn("SCRAM iteration count ({}) lower than recommended ({})", this.iterations, warnMinIterations); } + this.sid = sid; this.serverNonce = r; this.state = State.SERVER_FIRST_RCVD; this.complete = false; zeroAndClearExpectedV(); return; } + if (this.state != State.CLIENT_FINAL_SENT) { + final State currentState = this.state; + zeroAndClearExpectedV(); + this.state = State.FAILED; + throw new AuthenticationException("SCRAM final response out of sequence: " + currentState); + } + + final String sid = params.get("sid"); + if (sid == null || sid.isEmpty()) { + zeroAndClearExpectedV(); + this.state = State.FAILED; + throw new MalformedChallengeException("SCRAM Authentication-Info missing sid"); + } + if (this.sid == null || !this.sid.equals(sid)) { + zeroAndClearExpectedV(); + this.state = State.FAILED; + throw new AuthenticationException("SCRAM sid mismatch"); + } + // --- final-response path (Authentication-Info on any status) --- // For Authentication-Info, RFC 7804 does NOT mandate a scheme token; do NOT enforce scheme name here. final String data = params.get("data"); if (data == null) { - return; + zeroAndClearExpectedV(); + this.state = State.FAILED; + throw new MalformedChallengeException("SCRAM Authentication-Info missing data"); + } + final String decoded; + try { + decoded = b64ToString(data); + } catch (final MalformedChallengeException ex) { + zeroAndClearExpectedV(); + this.state = State.FAILED; + throw ex; + } + final Map<String, String> attrs; + try { + attrs = parseAttrs(decoded); + } catch (final MalformedChallengeException ex) { + zeroAndClearExpectedV(); + this.state = State.FAILED; + throw ex; } - final String decoded = b64ToString(data); - final Map<String, String> attrs = parseAttrs(decoded); final String err = attrs.get("e"); if (err != null) { this.state = State.FAILED; @@ -303,7 +388,9 @@ public void processChallenge( } final String vB64 = attrs.get("v"); if (vB64 == null) { - return; + zeroAndClearExpectedV(); + this.state = State.FAILED; + throw new MalformedChallengeException("SCRAM Authentication-Info missing v"); } // compare 'v' in constant time; treat bad base64 for v as a signature mismatch (tests expect "signature") @@ -335,7 +422,7 @@ public void processChallenge( */ @Override public boolean isChallengeComplete() { - return this.complete || this.state == State.COMPLETE || this.state == State.FAILED; + return this.state == State.FAILED; } /** @@ -486,9 +573,7 @@ private String buildClientFinalAndExpectV() throws AuthenticationException { final StringBuilder sb = new StringBuilder(64); sb.append(StandardAuthScheme.SCRAM_SHA_256).append(' '); - if (this.sid != null) { - sb.append("sid=").append(quoteParam(this.sid)).append(", "); - } + sb.append("sid=").append(quoteParam(this.sid)).append(", "); sb.append("data=").append(quoteParam(data)); // quoted this.state = State.CLIENT_FINAL_SENT;
httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java+0 −1 modified@@ -69,7 +69,6 @@ public class DefaultAuthenticationStrategy implements AuthenticationStrategy { private static final List<String> DEFAULT_SCHEME_PRIORITY = Collections.unmodifiableList(Arrays.asList( StandardAuthScheme.BEARER, - StandardAuthScheme.SCRAM_SHA_256, StandardAuthScheme.DIGEST, StandardAuthScheme.BASIC));
httpclient5/src/test/java/org/apache/hc/client5/http/examples/ClientScramAuthentication.java+81 −0 added@@ -0,0 +1,81 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ +package org.apache.hc.client5.http.examples; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.message.StatusLine; + +/** + * An example of how to explicitly enable {@code SCRAM-SHA-256} authentication. + * SCRAM-SHA-256 is not part of the default auth scheme preference list and + * must be opted into by placing it on the preferred auth schemes list of + * the {@link RequestConfig}. + * + * @since 5.7 + */ +public class ClientScramAuthentication { + + public static void main(final String[] args) throws Exception { + final List<String> priority = Collections.unmodifiableList(Arrays.asList( + StandardAuthScheme.SCRAM_SHA_256, + StandardAuthScheme.BEARER)); + + final HttpHost target = new HttpHost("http", "httpbin.org", 80); + + try (final CloseableHttpClient httpclient = HttpClients.custom() + .setDefaultRequestConfig(RequestConfig.custom() + .setTargetPreferredAuthSchemes(priority) + .setProxyPreferredAuthSchemes(priority) + .build()) + .setDefaultCredentialsProvider(CredentialsProviderBuilder.create() + .add(target, "user", "passwd".toCharArray()) + .build()) + .build()) { + + final HttpGet httpget = new HttpGet("http://httpbin.org/basic-auth/user/passwd"); + + System.out.println("Executing request " + httpget.getMethod() + " " + httpget.getUri()); + httpclient.execute(httpget, response -> { + System.out.println("----------------------------------------"); + System.out.println(httpget + "->" + new StatusLine(response)); + EntityUtils.consume(response.getEntity()); + return null; + }); + } + } +}
httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestAuthenticationHandler.java+46 −0 modified@@ -29,6 +29,7 @@ import java.util.LinkedList; import java.util.Queue; +import org.apache.hc.client5.http.auth.AuthChallenge; import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.AuthScheme; import org.apache.hc.client5.http.auth.AuthSchemeFactory; @@ -48,6 +49,7 @@ import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.config.Lookup; import org.apache.hc.core5.http.config.RegistryBuilder; import org.apache.hc.core5.http.message.BasicHeader; @@ -58,6 +60,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Answers; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; class TestAuthenticationHandler { @@ -481,4 +484,47 @@ void testAuthSuccessConnectionBased() throws Exception { Mockito.any(HttpContext.class)); } + private static String getParam(final AuthChallenge authChallenge, final String name) { + for (final NameValuePair param : authChallenge.getParams()) { + if (param.getName().equalsIgnoreCase(name)) { + return param.getValue(); + } + } + return null; + } + + @Test + void testAuthenticationInfoProcessedOnSuccessResponse() throws Exception { + final HttpHost host = new HttpHost("somehost", 80); + final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK, "OK"); + response.addHeader(new BasicHeader("Authentication-Info", "sid=\"sid-1\", data=\"dj1hYmM\"")); + + final AuthScheme authScheme = Mockito.mock(AuthScheme.class); + Mockito.when(authScheme.getName()).thenReturn(StandardAuthScheme.SCRAM_SHA_256); + Mockito.when(authScheme.isChallengeExpected()).thenReturn(Boolean.TRUE); + Mockito.when(authScheme.isChallengeComplete()).thenReturn(Boolean.FALSE); + + this.authExchange.select(authScheme); + this.authExchange.setState(AuthExchange.State.HANDSHAKE); + + final DefaultAuthenticationStrategy authStrategy = new DefaultAuthenticationStrategy(); + + Assertions.assertFalse(this.httpAuthenticator.handleResponse( + host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); + Assertions.assertEquals(AuthExchange.State.SUCCESS, this.authExchange.getState()); + + final ArgumentCaptor<AuthChallenge> challengeCaptor = ArgumentCaptor.forClass(AuthChallenge.class); + Mockito.verify(authScheme).processChallenge( + Mockito.eq(host), + Mockito.eq(false), + challengeCaptor.capture(), + Mockito.same(this.context)); + + final AuthChallenge challenge = challengeCaptor.getValue(); + Assertions.assertNotNull(challenge); + Assertions.assertEquals(StandardAuthScheme.SCRAM_SHA_256, challenge.getSchemeName()); + Assertions.assertEquals("sid-1", getParam(challenge, "sid")); + Assertions.assertEquals("dj1hYmM", getParam(challenge, "data")); + } + }
httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestScramScheme.java+201 −58 modified@@ -24,7 +24,6 @@ * <http://www.apache.org/>. * */ - package org.apache.hc.client5.http.impl.auth; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -63,7 +62,7 @@ final class TestScramScheme { private static final String REALM = "s1"; private static final String USER = "u"; private static final String PASS = "p"; - + private static final String SID = "sid-1"; static String b64(final byte[] b) { return Base64.getEncoder().withoutPadding().encodeToString(b); @@ -73,10 +72,6 @@ static byte[] b64d(final String s) { return Base64.getDecoder().decode(s); } - static String b64s(final String s) { - return b64(s.getBytes(StandardCharsets.UTF_8)); - } - static String deb64s(final String s) { return new String(b64d(s), StandardCharsets.UTF_8); } @@ -86,7 +81,9 @@ static Map<String, String> parseCsvAttrs(final String s) { int i = 0; while (i < s.length()) { final int eq = s.indexOf('=', i); - if (eq < 0) break; + if (eq < 0) { + break; + } final String k = s.substring(i, eq); i = eq + 1; final StringBuilder v = new StringBuilder(); @@ -105,7 +102,6 @@ static Map<String, String> parseCsvAttrs(final String s) { } static Map<String, String> splitHeader(final String header) { - // "SCRAM-SHA-256 realm="...", data="..."" -> name/value map (lowercase keys) final int sp = header.indexOf(' '); final String params = header.substring(sp + 1); final Map<String, String> map = new HashMap<>(); @@ -136,7 +132,6 @@ static byte[] hmac(final byte[] key, final String msg) throws GeneralSecurityExc return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8)); } - @Test void strictScram_fullRoundtrip_completes() throws Exception { final ScramScheme scheme = new ScramScheme(); @@ -145,40 +140,36 @@ void strictScram_fullRoundtrip_completes() throws Exception { new UsernamePasswordCredentials(USER, PASS.toCharArray())); final HttpClientContext ctx = HttpClientContext.create(); - // 401 announce (no data) scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), new BasicNameValuePair("realm", REALM)), ctx); assertTrue(scheme.isResponseReady(HOST, creds, ctx)); - // Authorization (client-first) final String authz1 = scheme.generateAuthResponse(HOST, null, ctx); final Map<String, String> h1 = splitHeader(authz1); assertEquals(REALM, h1.get("realm")); final String clientFirst = deb64s(h1.get("data")); - assertTrue(clientFirst.startsWith("n,,"), "GS2 header missing"); + assertTrue(clientFirst.startsWith("n,,")); final String clientFirstBare = clientFirst.substring("n,,".length()); - final Map<String, String> cf1 = parseCsvAttrs(clientFirstBare); - final String clientNonce = cf1.get("r"); + final String clientNonce = parseCsvAttrs(clientFirstBare).get("r"); assertNotNull(clientNonce); - // 401 server-first final String saltB64 = b64("salt-256".getBytes(StandardCharsets.UTF_8)); final int iters = 4096; final String serverFirst = "r=" + clientNonce + "XYZ,s=" + saltB64 + ",i=" + iters; scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), - new BasicNameValuePair("sid", "sid-1"), + new BasicNameValuePair("sid", SID), new BasicNameValuePair("data", b64(serverFirst.getBytes(StandardCharsets.UTF_8)))), ctx); final String authz2 = scheme.generateAuthResponse(HOST, null, ctx); final Map<String, String> h2 = splitHeader(authz2); - assertEquals("sid-1", h2.get("sid")); + assertEquals(SID, h2.get("sid")); final String clientFinal = deb64s(h2.get("data")); final Map<String, String> cf2 = parseCsvAttrs(clientFinal); - assertEquals("biws", cf2.get("c")); // Base64("n,,") + assertEquals("biws", cf2.get("c")); final String clientFinalNoProof = "c=" + cf2.get("c") + ",r=" + cf2.get("r"); final String authMessage = clientFirstBare + "," + serverFirst + "," + clientFinalNoProof; @@ -188,10 +179,56 @@ void strictScram_fullRoundtrip_completes() throws Exception { scheme.processChallenge(HOST, false, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), new BasicNameValuePair("data", b64(("v=" + vB64).getBytes(StandardCharsets.UTF_8)))), ctx); - assertTrue(scheme.isChallengeComplete()); + assertFalse(scheme.isChallengeComplete()); + } + + @Test + void strictScram_lowIterations_warnsButSucceeds() throws Exception { + final ScramScheme scheme = new ScramScheme(4096, 0, 100000, null); + final BasicCredentialsProvider creds = new BasicCredentialsProvider(); + creds.setCredentials(new AuthScope(HOST, REALM, scheme.getName()), + new UsernamePasswordCredentials(USER, PASS.toCharArray())); + final HttpClientContext ctx = HttpClientContext.create(); + + scheme.processChallenge(HOST, true, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("realm", REALM)), + ctx); + assertTrue(scheme.isResponseReady(HOST, creds, ctx)); + + final String authz1 = scheme.generateAuthResponse(HOST, null, ctx); + final String clientFirstBare = deb64s(splitHeader(authz1).get("data")).substring("n,,".length()); + final String clientNonce = parseCsvAttrs(clientFirstBare).get("r"); + + final String saltB64 = b64("salt-low".getBytes(StandardCharsets.UTF_8)); + final String serverFirst = "r=" + clientNonce + "Z,s=" + saltB64 + ",i=1024"; + scheme.processChallenge(HOST, true, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), + new BasicNameValuePair("data", b64(serverFirst.getBytes(StandardCharsets.UTF_8)))), + ctx); + + final String authz2 = scheme.generateAuthResponse(HOST, null, ctx); + final String clientFinal = deb64s(splitHeader(authz2).get("data")); + final Map<String, String> cf = parseCsvAttrs(clientFinal); + final String clientFinalNoProof = "c=" + cf.get("c") + ",r=" + cf.get("r"); + final String authMessage = clientFirstBare + "," + serverFirst + "," + clientFinalNoProof; + + final byte[] salted = pbkdf2(PASS.toCharArray(), b64d(saltB64), 1024, 32); + final byte[] serverKey = hmac(salted, "Server Key"); + final String vB64 = b64(hmac(serverKey, authMessage)); + + scheme.processChallenge(HOST, false, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), + new BasicNameValuePair("data", b64(("v=" + vB64).getBytes(StandardCharsets.UTF_8)))), + ctx); + + assertFalse(scheme.isChallengeComplete()); } @Test @@ -202,33 +239,56 @@ void strictScram_invalidServerNonce_rejectedAt401() throws Exception { new UsernamePasswordCredentials(USER, PASS.toCharArray())); final HttpClientContext ctx = HttpClientContext.create(); - // 401 announce scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), new BasicNameValuePair("realm", REALM)), ctx); assertTrue(scheme.isResponseReady(HOST, creds, ctx)); - - // Send client-first so the client nonce is generated and state is correct. - // We don't need the header content here. scheme.generateAuthResponse(HOST, null, ctx); - // Bad server-first: nonce does NOT start with the client nonce final String badServerFirst = "r=NOTPREFIXED,s=" + b64("salt".getBytes(StandardCharsets.UTF_8)) + ",i=4096"; - final AuthenticationException ex = assertThrows(AuthenticationException.class, () -> scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), new BasicNameValuePair("data", b64(badServerFirst.getBytes(StandardCharsets.UTF_8)))), ctx)); - assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("nonce")); + assertTrue(scheme.isChallengeComplete()); } + @Test + void strictScram_minIterations_enforced() throws Exception { + final ScramScheme scheme = new ScramScheme(4096, 4096, 100000, null); + final BasicCredentialsProvider creds = new BasicCredentialsProvider(); + creds.setCredentials(new AuthScope(HOST, REALM, scheme.getName()), + new UsernamePasswordCredentials(USER, PASS.toCharArray())); + final HttpClientContext ctx = HttpClientContext.create(); + + scheme.processChallenge(HOST, true, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("realm", REALM)), + ctx); + assertTrue(scheme.isResponseReady(HOST, creds, ctx)); + + final String authz1 = scheme.generateAuthResponse(HOST, null, ctx); + final String clientFirstBare = deb64s(splitHeader(authz1).get("data")).substring("n,,".length()); + final String clientNonce = parseCsvAttrs(clientFirstBare).get("r"); + + final String serverFirst = "r=" + clientNonce + "Z,s=" + b64("salt".getBytes(StandardCharsets.UTF_8)) + ",i=1024"; + final AuthenticationException ex = assertThrows(AuthenticationException.class, () -> + scheme.processChallenge(HOST, true, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), + new BasicNameValuePair("data", b64(serverFirst.getBytes(StandardCharsets.UTF_8)))), + ctx)); + assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("iteration")); + assertTrue(scheme.isChallengeComplete()); + } @Test - void strictScram_lowIterations_warnsButSucceeds() throws Exception { - final ScramScheme scheme = new ScramScheme(4096, 0, null); // warn only + void strictScram_maxIterations_enforced() throws Exception { + final ScramScheme scheme = new ScramScheme(4096, 0, 8192, null); final BasicCredentialsProvider creds = new BasicCredentialsProvider(); creds.setCredentials(new AuthScope(HOST, REALM, scheme.getName()), new UsernamePasswordCredentials(USER, PASS.toCharArray())); @@ -244,11 +304,67 @@ void strictScram_lowIterations_warnsButSucceeds() throws Exception { final String clientFirstBare = deb64s(splitHeader(authz1).get("data")).substring("n,,".length()); final String clientNonce = parseCsvAttrs(clientFirstBare).get("r"); - // server-first with i=1024 (below warn threshold) - final String saltB64 = b64("salt-low".getBytes(StandardCharsets.UTF_8)); - final String serverFirst = "r=" + clientNonce + "Z,s=" + saltB64 + ",i=1024"; + final String serverFirst = "r=" + clientNonce + "Z,s=" + b64("salt".getBytes(StandardCharsets.UTF_8)) + ",i=20000"; + final AuthenticationException ex = assertThrows(AuthenticationException.class, () -> + scheme.processChallenge(HOST, true, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), + new BasicNameValuePair("data", b64(serverFirst.getBytes(StandardCharsets.UTF_8)))), + ctx)); + assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("maximum")); + assertTrue(scheme.isChallengeComplete()); + } + + @Test + void strictScram_missingSidInServerFirst_isMalformed() throws Exception { + final ScramScheme scheme = new ScramScheme(); + final BasicCredentialsProvider creds = new BasicCredentialsProvider(); + creds.setCredentials(new AuthScope(HOST, REALM, scheme.getName()), + new UsernamePasswordCredentials(USER, PASS.toCharArray())); + final HttpClientContext ctx = HttpClientContext.create(); + + scheme.processChallenge(HOST, true, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("realm", REALM)), + ctx); + assertTrue(scheme.isResponseReady(HOST, creds, ctx)); + final String authz1 = scheme.generateAuthResponse(HOST, null, ctx); + final String clientFirstBare = deb64s(splitHeader(authz1).get("data")).substring("n,,".length()); + final String clientNonce = parseCsvAttrs(clientFirstBare).get("r"); + + final String serverFirst = "r=" + clientNonce + "Z,s=" + b64("salt".getBytes(StandardCharsets.UTF_8)) + ",i=4096"; + final MalformedChallengeException ex = assertThrows(MalformedChallengeException.class, () -> + scheme.processChallenge(HOST, true, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("data", b64(serverFirst.getBytes(StandardCharsets.UTF_8)))), + ctx)); + assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("sid")); + assertTrue(scheme.isChallengeComplete()); + } + + @Test + void strictScram_authInfo_missingSid_fails() throws Exception { + final ScramScheme scheme = new ScramScheme(); + final BasicCredentialsProvider creds = new BasicCredentialsProvider(); + creds.setCredentials(new AuthScope(HOST, REALM, scheme.getName()), + new UsernamePasswordCredentials(USER, PASS.toCharArray())); + final HttpClientContext ctx = HttpClientContext.create(); + scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("realm", REALM)), + ctx); + assertTrue(scheme.isResponseReady(HOST, creds, ctx)); + + final String authz1 = scheme.generateAuthResponse(HOST, null, ctx); + final String clientFirstBare = deb64s(splitHeader(authz1).get("data")).substring("n,,".length()); + final String clientNonce = parseCsvAttrs(clientFirstBare).get("r"); + + final String saltB64 = b64("salt".getBytes(StandardCharsets.UTF_8)); + final String serverFirst = "r=" + clientNonce + "Z,s=" + saltB64 + ",i=4096"; + scheme.processChallenge(HOST, true, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), new BasicNameValuePair("data", b64(serverFirst.getBytes(StandardCharsets.UTF_8)))), ctx); @@ -257,23 +373,22 @@ void strictScram_lowIterations_warnsButSucceeds() throws Exception { final Map<String, String> cf = parseCsvAttrs(clientFinal); final String clientFinalNoProof = "c=" + cf.get("c") + ",r=" + cf.get("r"); final String authMessage = clientFirstBare + "," + serverFirst + "," + clientFinalNoProof; - - final byte[] salted = pbkdf2(PASS.toCharArray(), b64d(saltB64), 1024, 32); + final byte[] salted = pbkdf2(PASS.toCharArray(), b64d(saltB64), 4096, 32); final byte[] serverKey = hmac(salted, "Server Key"); final String vB64 = b64(hmac(serverKey, authMessage)); - // 2xx with v -> success - scheme.processChallenge(HOST, false, - new AuthChallenge(ChallengeType.TARGET, scheme.getName(), - new BasicNameValuePair("data", b64(("v=" + vB64).getBytes(StandardCharsets.UTF_8)))), - ctx); - + final MalformedChallengeException ex = assertThrows(MalformedChallengeException.class, () -> + scheme.processChallenge(HOST, false, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("data", b64(("v=" + vB64).getBytes(StandardCharsets.UTF_8)))), + ctx)); + assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("sid")); assertTrue(scheme.isChallengeComplete()); } @Test - void strictScram_minIterations_enforced() throws Exception { - final ScramScheme scheme = new ScramScheme(4096, 4096, null); // hard min 4096 + void strictScram_authInfo_sidMismatch_fails() throws Exception { + final ScramScheme scheme = new ScramScheme(); final BasicCredentialsProvider creds = new BasicCredentialsProvider(); creds.setCredentials(new AuthScope(HOST, REALM, scheme.getName()), new UsernamePasswordCredentials(USER, PASS.toCharArray())); @@ -289,14 +404,31 @@ void strictScram_minIterations_enforced() throws Exception { final String clientFirstBare = deb64s(splitHeader(authz1).get("data")).substring("n,,".length()); final String clientNonce = parseCsvAttrs(clientFirstBare).get("r"); - // server-first with i=1024 (below hard min) -> fail immediately at processChallenge(401) - final String serverFirst = "r=" + clientNonce + "Z,s=" + b64("salt".getBytes(StandardCharsets.UTF_8)) + ",i=1024"; + final String saltB64 = b64("salt".getBytes(StandardCharsets.UTF_8)); + final String serverFirst = "r=" + clientNonce + "Z,s=" + saltB64 + ",i=4096"; + scheme.processChallenge(HOST, true, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), + new BasicNameValuePair("data", b64(serverFirst.getBytes(StandardCharsets.UTF_8)))), + ctx); + + final String authz2 = scheme.generateAuthResponse(HOST, null, ctx); + final String clientFinal = deb64s(splitHeader(authz2).get("data")); + final Map<String, String> cf = parseCsvAttrs(clientFinal); + final String clientFinalNoProof = "c=" + cf.get("c") + ",r=" + cf.get("r"); + final String authMessage = clientFirstBare + "," + serverFirst + "," + clientFinalNoProof; + final byte[] salted = pbkdf2(PASS.toCharArray(), b64d(saltB64), 4096, 32); + final byte[] serverKey = hmac(salted, "Server Key"); + final String vB64 = b64(hmac(serverKey, authMessage)); + final AuthenticationException ex = assertThrows(AuthenticationException.class, () -> - scheme.processChallenge(HOST, true, + scheme.processChallenge(HOST, false, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), - new BasicNameValuePair("data", b64(serverFirst.getBytes(StandardCharsets.UTF_8)))), + new BasicNameValuePair("sid", "sid-other"), + new BasicNameValuePair("data", b64(("v=" + vB64).getBytes(StandardCharsets.UTF_8)))), ctx)); - assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("iteration")); + assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("sid")); + assertTrue(scheme.isChallengeComplete()); } @Test @@ -320,19 +452,20 @@ void strictScram_authInfo_mismatchV_fails() throws Exception { final String serverFirst = "r=" + clientNonce + "Z,s=" + b64("salt".getBytes(StandardCharsets.UTF_8)) + ",i=4096"; scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), new BasicNameValuePair("data", b64(serverFirst.getBytes(StandardCharsets.UTF_8)))), ctx); - // client-final scheme.generateAuthResponse(HOST, null, ctx); - // 2xx with wrong v final MalformedChallengeException ex = assertThrows(MalformedChallengeException.class, () -> scheme.processChallenge(HOST, false, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), new BasicNameValuePair("data", b64("v=WRONG".getBytes(StandardCharsets.UTF_8)))), ctx)); assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("signature")); + assertTrue(scheme.isChallengeComplete()); } @Test @@ -356,18 +489,20 @@ void strictScram_authInfo_errorE_fails() throws Exception { final String serverFirst = "r=" + clientNonce + "Z,s=" + b64("salt".getBytes(StandardCharsets.UTF_8)) + ",i=4096"; scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), new BasicNameValuePair("data", b64(serverFirst.getBytes(StandardCharsets.UTF_8)))), ctx); scheme.generateAuthResponse(HOST, null, ctx); - // 2xx with e= final AuthenticationException ex = assertThrows(AuthenticationException.class, () -> scheme.processChallenge(HOST, false, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), new BasicNameValuePair("data", b64("e=server-fail".getBytes(StandardCharsets.UTF_8)))), ctx)); assertTrue(ex.getMessage().contains("server error")); + assertTrue(scheme.isChallengeComplete()); } @Test @@ -378,9 +513,11 @@ void strictScram_badBase64In401Data_isMalformed() { final MalformedChallengeException ex = assertThrows(MalformedChallengeException.class, () -> scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), new BasicNameValuePair("data", "%%%not-base64%%%")), ctx)); assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("base64")); + assertTrue(scheme.isChallengeComplete()); } @Test @@ -391,9 +528,11 @@ void strictScram_missingAttrsInServerFirst_isMalformed() { final MalformedChallengeException ex = assertThrows(MalformedChallengeException.class, () -> scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), new BasicNameValuePair("data", b64("r=only".getBytes(StandardCharsets.UTF_8)))), ctx)); assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("missing")); + assertTrue(scheme.isChallengeComplete()); } @Test @@ -404,7 +543,6 @@ void testPreemptiveAuthentication() throws Exception { new UsernamePasswordCredentials(USER, PASS.toCharArray())); final HttpClientContext ctx = HttpClientContext.create(); - // Test that we can generate a response without receiving a challenge first assertTrue(scheme.isResponseReady(HOST, creds, ctx)); final String response = scheme.generateAuthResponse(HOST, null, ctx); assertNotNull(response); @@ -427,31 +565,38 @@ void testInvalidBase64InAuthInfo() throws Exception { new UsernamePasswordCredentials(USER, PASS.toCharArray())); final HttpClientContext ctx = HttpClientContext.create(); - // Go through the initial flow scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), new BasicNameValuePair("realm", REALM)), ctx); + assertTrue(scheme.isResponseReady(HOST, creds, ctx)); + final String authz1 = scheme.generateAuthResponse(HOST, null, ctx); + final String clientFirstBare = deb64s(splitHeader(authz1).get("data")).substring("n,,".length()); + final String clientNonce = parseCsvAttrs(clientFirstBare).get("r"); + + final String serverFirst = "r=" + clientNonce + "Z,s=" + b64("salt".getBytes(StandardCharsets.UTF_8)) + ",i=4096"; + scheme.processChallenge(HOST, true, + new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), + new BasicNameValuePair("data", b64(serverFirst.getBytes(StandardCharsets.UTF_8)))), + ctx); scheme.generateAuthResponse(HOST, null, ctx); - // Test with invalid base64 in Authentication-Info final MalformedChallengeException ex = assertThrows(MalformedChallengeException.class, () -> scheme.processChallenge(HOST, false, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), + new BasicNameValuePair("sid", SID), new BasicNameValuePair("data", "invalid-base64")), ctx)); assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("base64")); + assertTrue(scheme.isChallengeComplete()); } @Test void testInvalidStateTransition() throws Exception { final ScramScheme scheme = new ScramScheme(); - final BasicCredentialsProvider creds = new BasicCredentialsProvider(); - creds.setCredentials(new AuthScope(HOST, REALM, scheme.getName()), - new UsernamePasswordCredentials(USER, PASS.toCharArray())); final HttpClientContext ctx = HttpClientContext.create(); - // Try to generate a response without proper state setup final AuthenticationException ex = assertThrows(AuthenticationException.class, () -> scheme.generateAuthResponse(HOST, null, ctx)); assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("sequence")); @@ -465,7 +610,6 @@ void testEmptyPassword() throws Exception { new UsernamePasswordCredentials(USER, "".toCharArray())); final HttpClientContext ctx = HttpClientContext.create(); - // This should work without throwing exceptions scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), new BasicNameValuePair("realm", REALM)), @@ -483,7 +627,6 @@ void testSpecialCharacters() throws Exception { new UsernamePasswordCredentials(specialUser, specialPass.toCharArray())); final HttpClientContext ctx = HttpClientContext.create(); - // Test the full flow with special characters scheme.processChallenge(HOST, true, new AuthChallenge(ChallengeType.TARGET, scheme.getName(), new BasicNameValuePair("realm", REALM)),
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- www.openwall.com/lists/oss-security/2026/04/22/5nvdMailing ListThird Party AdvisoryWEB
- github.com/advisories/GHSA-v468-qcjx-r72wghsaADVISORY
- lists.apache.org/thread/tfmgv86xr0z1y096vs3z0y315t1v3o97nvdMailing ListVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-40542ghsaADVISORY
- github.com/apache/httpcomponents-client/commit/726eac2323d370435d8afca1e0540aa099927f18ghsaWEB
News mentions
0No linked articles in our index yet.