@@ -80,6 +80,9 @@ public final class ScramScheme implements AuthScheme {
8080
8181 private static final Logger LOG = LoggerFactory .getLogger (ScramScheme .class );
8282
83+ private static final int DEFAULT_WARN_MIN_ITERATIONS = 4096 ;
84+ private static final int DEFAULT_MAX_ITERATIONS_ALLOWED = 100000 ;
85+
8386 // RFC 7804 / RFC 5802 fixed no-CB GS2 header and its base64 value for 'c='
8487 private static final String GS2_HEADER = "n,," ;
8588 private static final String C_BIND_B64 = "biws" ; // base64("n,,")
@@ -100,6 +103,7 @@ private enum State {
100103 private final SecureRandom secureRandom ;
101104 private final int warnMinIterations ;
102105 private final int minIterationsRequired ;
106+ private final int maxIterationsAllowed ;
103107
104108 private State state = State .INIT ;
105109 private boolean complete ;
@@ -128,7 +132,7 @@ private enum State {
128132 * @since 5.6
129133 */
130134 public ScramScheme () {
131- this (4096 , 0 , null );
135+ this (DEFAULT_WARN_MIN_ITERATIONS , 0 , DEFAULT_MAX_ITERATIONS_ALLOWED , null );
132136 }
133137
134138 /**
@@ -140,8 +144,26 @@ public ScramScheme() {
140144 * @since 5.6
141145 */
142146 public ScramScheme (final int warnMinIterations , final int minIterationsRequired , final SecureRandom rnd ) {
147+ this (warnMinIterations , minIterationsRequired , DEFAULT_MAX_ITERATIONS_ALLOWED , rnd );
148+ }
149+
150+ /**
151+ * Constructor with custom iteration policy.
152+ *
153+ * @param warnMinIterations warn if iteration count is lower than this (0 disables warnings)
154+ * @param minIterationsRequired fail if iteration count is lower than this (0 disables enforcement)
155+ * @param maxIterationsAllowed fail if iteration count is greater than this (must be positive)
156+ * @param rnd optional secure random source (null uses system default)
157+ * @since 5.6
158+ */
159+ public ScramScheme (
160+ final int warnMinIterations ,
161+ final int minIterationsRequired ,
162+ final int maxIterationsAllowed ,
163+ final SecureRandom rnd ) {
143164 this .warnMinIterations = Math .max (0 , warnMinIterations );
144165 this .minIterationsRequired = Math .max (0 , minIterationsRequired );
166+ this .maxIterationsAllowed = Args .positive (maxIterationsAllowed , "Max iterations allowed" );
145167 this .secureRandom = rnd != null ? rnd : new SecureRandom ();
146168 }
147169
@@ -206,9 +228,10 @@ public void processChallenge(
206228 Args .notNull (context , "HTTP context" );
207229
208230 if (authChallenge == null ) {
209- if (!challenged ) {
210- // Final response with no Authentication-Info: nothing to do
211- return ;
231+ if (!challenged && this .state == State .CLIENT_FINAL_SENT ) {
232+ zeroAndClearExpectedV ();
233+ this .state = State .FAILED ;
234+ throw new AuthenticationException ("Missing SCRAM Authentication-Info" );
212235 }
213236 throw new MalformedChallengeException ("Null SCRAM challenge" );
214237 }
@@ -232,10 +255,32 @@ public void processChallenge(
232255 return ;
233256 }
234257
258+ final String sid = params .get ("sid" );
259+ if (sid == null || sid .isEmpty ()) {
260+ zeroAndClearExpectedV ();
261+ this .state = State .FAILED ;
262+ throw new MalformedChallengeException ("SCRAM server-first missing sid" );
263+ }
264+
235265 // server-first (data present)
236- final String decoded = b64ToString (data );
266+ final String decoded ;
267+ try {
268+ decoded = b64ToString (data );
269+ } catch (final MalformedChallengeException ex ) {
270+ zeroAndClearExpectedV ();
271+ this .state = State .FAILED ;
272+ throw ex ;
273+ }
237274 this .serverFirstRaw = decoded ;
238- final Map <String , String > attrs = parseAttrs (decoded );
275+
276+ final Map <String , String > attrs ;
277+ try {
278+ attrs = parseAttrs (decoded );
279+ } catch (final MalformedChallengeException ex ) {
280+ zeroAndClearExpectedV ();
281+ this .state = State .FAILED ;
282+ throw ex ;
283+ }
239284
240285 final String r = attrs .get ("r" );
241286 final String s = attrs .get ("s" );
@@ -249,7 +294,6 @@ public void processChallenge(
249294 throw new AuthenticationException ("SCRAM server nonce does not start with client nonce" );
250295 }
251296
252- this .sid = params .get ("sid" );
253297 try {
254298 this .salt = B64D .decode (s );
255299 if (this .salt .length == 0 ) {
@@ -274,25 +318,66 @@ public void processChallenge(
274318 throw new AuthenticationException (
275319 "SCRAM iteration count below required minimum: " + this .iterations + " < " + this .minIterationsRequired );
276320 }
321+ if (this .iterations > this .maxIterationsAllowed ) {
322+ this .state = State .FAILED ;
323+ throw new AuthenticationException (
324+ "SCRAM iteration count above allowed maximum: " + this .iterations + " > " + this .maxIterationsAllowed );
325+ }
277326 if (this .warnMinIterations > 0 && this .iterations < this .warnMinIterations && LOG .isWarnEnabled ()) {
278327 LOG .warn ("SCRAM iteration count ({}) lower than recommended ({})" , this .iterations , warnMinIterations );
279328 }
280329
330+ this .sid = sid ;
281331 this .serverNonce = r ;
282332 this .state = State .SERVER_FIRST_RCVD ;
283333 this .complete = false ;
284334 zeroAndClearExpectedV ();
285335 return ;
286336 }
287337
338+ if (this .state != State .CLIENT_FINAL_SENT ) {
339+ final State currentState = this .state ;
340+ zeroAndClearExpectedV ();
341+ this .state = State .FAILED ;
342+ throw new AuthenticationException ("SCRAM final response out of sequence: " + currentState );
343+ }
344+
345+ final String sid = params .get ("sid" );
346+ if (sid == null || sid .isEmpty ()) {
347+ zeroAndClearExpectedV ();
348+ this .state = State .FAILED ;
349+ throw new MalformedChallengeException ("SCRAM Authentication-Info missing sid" );
350+ }
351+ if (this .sid == null || !this .sid .equals (sid )) {
352+ zeroAndClearExpectedV ();
353+ this .state = State .FAILED ;
354+ throw new AuthenticationException ("SCRAM sid mismatch" );
355+ }
356+
288357 // --- final-response path (Authentication-Info on any status) ---
289358 // For Authentication-Info, RFC 7804 does NOT mandate a scheme token; do NOT enforce scheme name here.
290359 final String data = params .get ("data" );
291360 if (data == null ) {
292- return ;
361+ zeroAndClearExpectedV ();
362+ this .state = State .FAILED ;
363+ throw new MalformedChallengeException ("SCRAM Authentication-Info missing data" );
364+ }
365+ final String decoded ;
366+ try {
367+ decoded = b64ToString (data );
368+ } catch (final MalformedChallengeException ex ) {
369+ zeroAndClearExpectedV ();
370+ this .state = State .FAILED ;
371+ throw ex ;
372+ }
373+ final Map <String , String > attrs ;
374+ try {
375+ attrs = parseAttrs (decoded );
376+ } catch (final MalformedChallengeException ex ) {
377+ zeroAndClearExpectedV ();
378+ this .state = State .FAILED ;
379+ throw ex ;
293380 }
294- final String decoded = b64ToString (data );
295- final Map <String , String > attrs = parseAttrs (decoded );
296381 final String err = attrs .get ("e" );
297382 if (err != null ) {
298383 this .state = State .FAILED ;
@@ -303,7 +388,9 @@ public void processChallenge(
303388 }
304389 final String vB64 = attrs .get ("v" );
305390 if (vB64 == null ) {
306- return ;
391+ zeroAndClearExpectedV ();
392+ this .state = State .FAILED ;
393+ throw new MalformedChallengeException ("SCRAM Authentication-Info missing v" );
307394 }
308395
309396 // compare 'v' in constant time; treat bad base64 for v as a signature mismatch (tests expect "signature")
@@ -335,7 +422,7 @@ public void processChallenge(
335422 */
336423 @ Override
337424 public boolean isChallengeComplete () {
338- return this .complete || this . state == State . COMPLETE || this . state == State .FAILED ;
425+ return this .state == State .FAILED ;
339426 }
340427
341428 /**
@@ -486,9 +573,7 @@ private String buildClientFinalAndExpectV() throws AuthenticationException {
486573
487574 final StringBuilder sb = new StringBuilder (64 );
488575 sb .append (StandardAuthScheme .SCRAM_SHA_256 ).append (' ' );
489- if (this .sid != null ) {
490- sb .append ("sid=" ).append (quoteParam (this .sid )).append (", " );
491- }
576+ sb .append ("sid=" ).append (quoteParam (this .sid )).append (", " );
492577 sb .append ("data=" ).append (quoteParam (data )); // quoted
493578
494579 this .state = State .CLIENT_FINAL_SENT ;
0 commit comments