This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-codec.git
The following commit(s) were added to refs/heads/master by this push:
new 6837802c [CODEC-245] Reject malformed Sha2Crypt salt syntax
6837802c is described below
commit 6837802c8f23cb970513035c0fede41a1e30bb23
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 26 20:33:39 2026 +0000
[CODEC-245] Reject malformed Sha2Crypt salt syntax
Refactor tests.
---
.../apache/commons/codec/digest/Sha2CryptTest.java | 81 ++++++++++++++++++++++
.../commons/codec/digest/Sha512CryptTest.java | 9 ---
2 files changed, 81 insertions(+), 9 deletions(-)
diff --git a/src/test/java/org/apache/commons/codec/digest/Sha2CryptTest.java
b/src/test/java/org/apache/commons/codec/digest/Sha2CryptTest.java
index e94d4e6a..3e14e3bf 100644
--- a/src/test/java/org/apache/commons/codec/digest/Sha2CryptTest.java
+++ b/src/test/java/org/apache/commons/codec/digest/Sha2CryptTest.java
@@ -22,18 +22,87 @@ import static
org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import java.nio.charset.StandardCharsets;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
class Sha2CryptTest {
+ private static void assertSaltRejected(final int bits, final String salt) {
+ final byte[] key = "secret".getBytes(StandardCharsets.UTF_8);
+ assertThrowsExactly(IllegalArgumentException.class, () -> {
+ if (bits == 256) {
+ Sha2Crypt.sha256Crypt(key, salt);
+ } else {
+ Sha2Crypt.sha512Crypt(key, salt);
+ }
+ });
+ }
+
+ static Stream<Arguments> invalidSaltCharacters() {
+ return saltsForBothVariants(new String[][] {
+ // @formatter:off
+ { "Unicode after valid prefix", "rounds=1000$abcäöüäöü" },
+ { "space after valid prefix", "abc def" },
+ { "tab after valid prefix", "abc\tdef" },
+ { "punctuation after valid prefix", "abc!" },
+ { "invalid character after 16 valid characters",
"abcdefghijklmnop!" }
+ // @formatter:on
+ });
+ }
+
+ static Stream<Arguments> malformedRounds() {
+ return saltsForBothVariants(new String[][] {
+ // @formatter:off
+ { "incorrect rounds keyword", "notrounds=1000$asdfasdf" },
+ { "empty rounds", "rounds=$abc" },
+ { "non-numeric rounds", "rounds=abc$abc" },
+ { "negative rounds", "rounds=-1$abc" }
+ // @formatter:on
+ });
+ }
+
+ private static Stream<Arguments> saltsForBothVariants(final String[][]
cases) {
+ return Stream.of(256, 512).flatMap(bits -> Stream.of(cases)
+ .map(testCase -> Arguments.of(bits, Named.of(testCase[0],
(bits == 256 ? "$5$" : "$6$") + testCase[1]))));
+ }
+
+ static Stream<Arguments> trailingLineTerminators() {
+ return saltsForBothVariants(new String[][] {
+ // @formatter:off
+ { "LF", "rounds=1000$abc\n" },
+ { "CR", "rounds=1000$abc\r" },
+ { "CRLF", "rounds=1000$abc\r\n" },
+ { "NEL (U+0085)", "rounds=1000$abc\u0085" },
+ { "LINE SEPARATOR (U+2028)", "rounds=1000$abc\u2028" },
+ { "PARAGRAPH SEPARATOR (U+2029)", "rounds=1000$abc\u2029" }
+ // @formatter:on
+ });
+ }
+
+ @SuppressWarnings("deprecation")
@Test
void testCtor() {
assertNotNull(new Sha2Crypt());
}
+ @ParameterizedTest(name = "SHA-{0}: {1}")
+ @MethodSource("invalidSaltCharacters")
+ void testInvalidSaltCharactersRejected(final int bits, final String salt) {
+ assertSaltRejected(bits, salt);
+ }
+
+ @ParameterizedTest(name = "SHA-{0}: invalid salt prefix")
+ @ValueSource(ints = { 256, 512 })
+ void testInvalidSaltPrefixRejected(final int bits) {
+ assertSaltRejected(bits, "xx");
+ }
+
@ParameterizedTest
@ValueSource(ints = { 100_000, 1_000_000 })
void testLargeRounds(final int rounds) {
@@ -41,6 +110,12 @@ class Sha2CryptTest {
Crypt.crypt("anything".getBytes(StandardCharsets.UTF_8), salt);
}
+ @ParameterizedTest(name = "SHA-{0}: {1}")
+ @MethodSource("malformedRounds")
+ void testMalformedRoundsRejected(final int bits, final String salt) {
+ assertSaltRejected(bits, salt);
+ }
+
@Test
void testRoundsAboveCeilingRejected() {
assertThrowsExactly(IllegalArgumentException.class,
@@ -72,4 +147,10 @@ class Sha2CryptTest {
final String actual =
Sha2Crypt.sha512Crypt("secret".getBytes(StandardCharsets.UTF_8),
"$6$rounds=0000001000$abcdefghijklmnop");
assertEquals(expected, actual);
}
+
+ @ParameterizedTest(name = "SHA-{0}: {1}")
+ @MethodSource("trailingLineTerminators")
+ void testTrailingLineTerminatorsRejected(final int bits, final String
salt) {
+ assertSaltRejected(bits, salt);
+ }
}
diff --git a/src/test/java/org/apache/commons/codec/digest/Sha512CryptTest.java
b/src/test/java/org/apache/commons/codec/digest/Sha512CryptTest.java
index eaf2605c..76142635 100644
--- a/src/test/java/org/apache/commons/codec/digest/Sha512CryptTest.java
+++ b/src/test/java/org/apache/commons/codec/digest/Sha512CryptTest.java
@@ -47,15 +47,6 @@ class Sha512CryptTest {
assertEquals("$5$rounds=9999$abcd$Rh/8ngVh9oyuS6lL3.fsq.9xbvXJsfyKWxSjO2mPIa7",
Sha2Crypt.sha256Crypt("secret".getBytes(StandardCharsets.UTF_8),
"$5$rounds=9999$abcd"));
}
- @Test
- void testSha2CryptWrongSalt() {
- assertThrows(IllegalArgumentException.class, () ->
Sha2Crypt.sha512Crypt("secret".getBytes(StandardCharsets.UTF_8), "xx"));
- assertThrows(IllegalArgumentException.class,
- () ->
Sha2Crypt.sha256Crypt("secret".getBytes(StandardCharsets.UTF_8),
"$5$notrounds=1000$asdfasdf"));
- assertThrows(IllegalArgumentException.class,
- () ->
Sha2Crypt.sha512Crypt("secret".getBytes(StandardCharsets.UTF_8),
"$6$rounds=1000$abcäöüäöü"));
- }
-
@Test
void testSha512CryptBytes() {
// An empty byte array equals an empty String