Skip to content

Commit e1fa8df

Browse files
committed
Let S3Mock validate bucket names according to AWS rules
1 parent a6dc591 commit e1fa8df

File tree

3 files changed

+121
-2
lines changed

3 files changed

+121
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
166166
Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration.
167167

168168
* Features and fixes
169-
* TBD
169+
* Let S3Mock validate bucket names according to AWS rules
170170
* Refactorings
171171
* Let TaggingHeaderConverter convert XML tags
172172
* Let Spring convert StorageClass in postObject

server/src/main/java/com/adobe/testing/s3mock/service/BucketService.java

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,23 @@
6363
import java.util.Objects;
6464
import java.util.UUID;
6565
import java.util.concurrent.ConcurrentHashMap;
66+
import java.util.regex.Pattern;
6667
import org.jspecify.annotations.Nullable;
6768
import software.amazon.awssdk.utils.http.SdkHttpUtils;
6869

6970
public class BucketService {
7071
private final Map<String, String> listObjectsPagingStateCache = new ConcurrentHashMap<>();
7172
private final Map<String, String> listBucketsPagingStateCache = new ConcurrentHashMap<>();
73+
// Validation patterns per S3 bucket naming rules
74+
private static final Pattern ALLOWED_CHARS_AND_LENGTH =
75+
Pattern.compile("^[a-z0-9.-]{3,63}$");
76+
private static final Pattern STARTS_AND_ENDS_WITH_ALNUM =
77+
Pattern.compile("^[a-z0-9].*[a-z0-9]$");
78+
private static final Pattern ADJACENT_DOTS =
79+
Pattern.compile("\\.\\.");
80+
private static final Pattern IP_LIKE_FOUR_PARTS =
81+
Pattern.compile("^(\\d{1,3}\\.){3}\\d{1,3}$");
82+
7283
private final BucketStore bucketStore;
7384
private final ObjectStore objectStore;
7485

@@ -498,12 +509,73 @@ public void verifyBucketObjectLockEnabled(String bucketName) {
498509
}
499510

500511
/**
512+
* Validates S3 bucket names according to the documented constraints.
501513
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html">API Reference Bucket Naming</a>.
502514
*/
503515
public void verifyBucketNameIsAllowed(String bucketName) {
504-
if (!bucketName.matches("[a-z0-9.-]+")) {
516+
if (bucketName.isBlank()) {
517+
throw INVALID_BUCKET_NAME;
518+
}
519+
520+
// Allowed chars and length (3..63)
521+
if (!ALLOWED_CHARS_AND_LENGTH.matcher(bucketName).matches()) {
522+
throw INVALID_BUCKET_NAME;
523+
}
524+
525+
// Must start and end with a letter or number
526+
if (!STARTS_AND_ENDS_WITH_ALNUM.matcher(bucketName).matches()) {
527+
throw INVALID_BUCKET_NAME;
528+
}
529+
530+
// Must not contain two adjacent periods
531+
if (ADJACENT_DOTS.matcher(bucketName).find()) {
532+
throw INVALID_BUCKET_NAME;
533+
}
534+
535+
// Must not be formatted as an IP address (e.g., 192.168.5.4)
536+
if (IP_LIKE_FOUR_PARTS.matcher(bucketName).matches() && isValidIpv4(bucketName)) {
537+
throw INVALID_BUCKET_NAME;
538+
}
539+
540+
// Disallowed prefixes
541+
if (bucketName.startsWith("xn--")) {
542+
throw INVALID_BUCKET_NAME;
543+
}
544+
if (bucketName.startsWith("sthree-")) {
505545
throw INVALID_BUCKET_NAME;
506546
}
547+
if (bucketName.startsWith("amzn-s3-demo-")) {
548+
throw INVALID_BUCKET_NAME;
549+
}
550+
}
551+
552+
// Parses and validates IPv4 octets (0..255) to avoid false positives like 999.999.999.999
553+
private static boolean isValidIpv4(String s) {
554+
String[] parts = s.split("\\.");
555+
if (parts.length != 4) {
556+
return false;
557+
}
558+
for (String p : parts) {
559+
if (p.isEmpty() || p.length() > 3) {
560+
return false;
561+
}
562+
// Disallow leading plus/minus; Pattern already ensures digits only
563+
int val;
564+
try {
565+
val = Integer.parseInt(p);
566+
} catch (NumberFormatException e) {
567+
return false;
568+
}
569+
if (val < 0 || val > 255) {
570+
return false;
571+
}
572+
// Avoid leading zeros ambiguity like "01", but S3 only cares about "formatted as IP".
573+
// If you prefer to allow "01", remove this check.
574+
if (p.length() > 1 && p.startsWith("0")) {
575+
return false;
576+
}
577+
}
578+
return true;
507579
}
508580

509581
public void verifyBucketIsEmpty(String bucketName) {

server/src/test/kotlin/com/adobe/testing/s3mock/service/BucketServiceTest.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,51 @@ internal class BucketServiceTest : ServiceTestBase() {
221221
.isEqualTo(S3Exception.INVALID_BUCKET_NAME)
222222
}
223223

224+
@Test
225+
fun verifyBucketNameIsAllowed_multipleValid() {
226+
val max63 = "a".repeat(63)
227+
val samples = listOf(
228+
"abc",
229+
"a-b",
230+
"a.b",
231+
"my.bucket-name-1",
232+
"n0dots-or-underscores", // hyphens and digits allowed
233+
"a1b2c3",
234+
"a.b.c",
235+
"start1-end2",
236+
max63
237+
)
238+
samples.forEach { name ->
239+
iut.verifyBucketNameIsAllowed(name)
240+
}
241+
}
242+
243+
@Test
244+
fun verifyBucketNameIsAllowed_multipleInvalid() {
245+
val tooLong = "a".repeat(64)
246+
val samples = listOf(
247+
"", // blank
248+
"a", // too short
249+
"ab", // too short
250+
"Aaa", // uppercase not allowed
251+
"abc_", // underscore not allowed
252+
"-abc", // must start with alnum
253+
".abc", // must start with alnum
254+
"abc-", // must end with alnum
255+
"abc.", // must end with alnum
256+
"ab..cd", // adjacent periods
257+
"192.168.5.4", // formatted as IPv4
258+
"xn--punycode", // forbidden prefix
259+
"sthree-bucket", // forbidden prefix
260+
"amzn-s3-demo-foo", // forbidden prefix
261+
tooLong // > 63
262+
)
263+
samples.forEach { name ->
264+
assertThatThrownBy { iut.verifyBucketNameIsAllowed(name) }
265+
.isEqualTo(S3Exception.INVALID_BUCKET_NAME)
266+
}
267+
}
268+
224269
@Test
225270
fun testVerifyBucketDoesNotExist_success() {
226271
val bucketName = "bucket"
@@ -632,6 +677,8 @@ internal class BucketServiceTest : ServiceTestBase() {
632677
assertThat(out.objectVersions()).isNotEmpty()
633678
}
634679

680+
681+
635682
companion object {
636683
private const val TEST_BUCKET_NAME = "test-bucket"
637684

0 commit comments

Comments
 (0)