|
63 | 63 | import java.util.Objects; |
64 | 64 | import java.util.UUID; |
65 | 65 | import java.util.concurrent.ConcurrentHashMap; |
| 66 | +import java.util.regex.Pattern; |
66 | 67 | import org.jspecify.annotations.Nullable; |
67 | 68 | import software.amazon.awssdk.utils.http.SdkHttpUtils; |
68 | 69 |
|
69 | 70 | public class BucketService { |
70 | 71 | private final Map<String, String> listObjectsPagingStateCache = new ConcurrentHashMap<>(); |
71 | 72 | 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 | + |
72 | 83 | private final BucketStore bucketStore; |
73 | 84 | private final ObjectStore objectStore; |
74 | 85 |
|
@@ -498,12 +509,73 @@ public void verifyBucketObjectLockEnabled(String bucketName) { |
498 | 509 | } |
499 | 510 |
|
500 | 511 | /** |
| 512 | + * Validates S3 bucket names according to the documented constraints. |
501 | 513 | * <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html">API Reference Bucket Naming</a>. |
502 | 514 | */ |
503 | 515 | 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-")) { |
505 | 545 | throw INVALID_BUCKET_NAME; |
506 | 546 | } |
| 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; |
507 | 579 | } |
508 | 580 |
|
509 | 581 | public void verifyBucketIsEmpty(String bucketName) { |
|
0 commit comments