{"record":{"id":"f7c077e1a955afbc","repo":"spring-projects/spring-security","slug":"missing-salt-rounds","errorCode":null,"errorMessage":"Missing salt rounds","messagePattern":"Missing salt rounds","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java","lineNumber":644,"sourceCode":"\t\t}\n\n\t\tif (salt.charAt(0) != '$' || salt.charAt(1) != '2') {\n\t\t\tthrow new IllegalArgumentException(\"Invalid salt version\");\n\t\t}\n\t\tif (salt.charAt(2) == '$') {\n\t\t\toff = 3;\n\t\t}\n\t\telse {\n\t\t\tminor = salt.charAt(2);\n\t\t\tif ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$') {\n\t\t\t\tthrow new IllegalArgumentException(\"Invalid salt revision\");\n\t\t\t}\n\t\t\toff = 4;\n\t\t}\n\n\t\t// Extract number of rounds\n\t\tif (salt.charAt(off + 2) > '$') {\n\t\t\tthrow new IllegalArgumentException(\"Missing salt rounds\");\n\t\t}\n\n\t\tif (off == 4 && saltLength < 29) {\n\t\t\tthrow new IllegalArgumentException(\"Invalid salt\");\n\t\t}\n\t\trounds = Integer.parseInt(salt.substring(off, off + 2));\n\n\t\treal_salt = salt.substring(off + 3, off + 25);\n\t\tsaltb = decode_base64(real_salt, BCRYPT_SALT_LEN);\n\n\t\tif (minor >= 'a') {\n\t\t\tpasswordb = Arrays.copyOf(passwordb, passwordb.length + 1);\n\t\t}\n\n\t\tB = new BCrypt();\n\t\thashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0, for_check);\n\n\t\trs.append(\"$2\");","sourceCodeStart":626,"sourceCodeEnd":662,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java#L626-L662","documentation":"hashpw() reads the two-digit cost factor immediately after the revision separator and requires the character at off+2 (which should terminate the cost field) to be <= '$'. If a digit or other character appears where the '$' separator is expected, the cost field is malformed and \"Missing salt rounds\" is thrown.","triggerScenarios":"Salt strings like $2a$1x$... or $2a$123$... where the two rounds digits are not followed by '$', e.g. a three-digit cost ($2a$100$) or a corrupted middle segment.","commonSituations":"Hand-editing hashes to change cost; hashes from libraries supporting 3-digit costs; truncation/concatenation bugs when storing hashes; typos when hardcoding a salt in tests.","solutions":["Use a valid full salt string: $2a$NN$ where NN is a two-digit cost 04-31 followed by '$'","Generate the salt via BCrypt.gensalt(cost) instead of writing it by hand","Never edit the cost portion of a stored hash; re-hash instead"],"exampleFix":"// before\nString salt = \"$2a$100$\" + rawSalt;\n// after\nString salt = BCrypt.gensalt(10); // $2a$10$<22 chars>","handlingStrategy":"validation","validationCode":"if (!salt.matches(\"^\\\\$2[abxy]\\\\$\\\\d{2}\\\\$\")) {\n    throw new IllegalArgumentException(\"Salt must contain a two-digit cost followed by $\");\n}","typeGuard":"boolean hasTwoDigitCost(String s) {\n    return s.length() > 6 && Character.isDigit(s.charAt(4)) && Character.isDigit(s.charAt(5)) && s.charAt(6) == '$';\n}","tryCatchPattern":"try {\n    hash = BCrypt.hashpw(pw, salt);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().equals(\"Missing salt rounds\")) { /* reject malformed hash */ }\n}","preventionTips":["Remember bcrypt cost is exactly two digits (04-31); 3-digit costs are not supported","Regenerate hashes rather than editing the cost field in place"],"tags":["bcrypt","spring-security","salt","rounds","format"],"backgroundTag":"invalid-argument-format","analyzedSha":"96852e8860138a482cb13d1479573f24ff6443c6","analyzedAt":"2026-09-10T23:25:23.477Z","contentChangedAt":"2026-09-10T23:25:23.477Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}