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

PackageAffected versionsPatched versions
org.apache.httpcomponents.client5:httpclient5Maven
>= 5.6-alpha1, < 5.6.15.6.1

Affected products

1

Patches

1
726eac2323d3

Fix SCRAM final response handling

https://github.com/apache/httpcomponents-clientArturo BernalApr 15, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.