Skip to content

Commit 828d1ef

Browse files
committed
introduce safe characters per AWS guidelines
1 parent 0038591 commit 828d1ef

2 files changed

Lines changed: 50 additions & 6 deletions

File tree

src/java/org/apache/cassandra/service/snapshot/SnapshotOptions.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ public class SnapshotOptions
4343
{
4444
public static final String SKIP_FLUSH = "skipFlush";
4545
public static final String TTL = "ttl";
46+
47+
// AWS S3 "Safe characters" for object keys:
48+
// 0-9 a-z A-Z ! - _ . * ' ( )
49+
// See https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html#object-key-guidelines
50+
// Hyphen is placed last in the character class, so it stays literal and never becomes a range operator.
51+
private static final Pattern SAFE_SNAPSHOT_NAME = Pattern.compile("[a-zA-Z0-9!_.*'()-]+");
4652
public final SnapshotType type;
4753
public final String tag;
4854
public final DurationSpec.IntSecondsBound ttl;
@@ -223,12 +229,23 @@ private void validateTag(String tag)
223229
FILENAME_LENGTH, resolvedSnapshotname.length(), resolvedSnapshotname));
224230
}
225231

226-
// "-" is important, we are already generating system snapshots with "-" in them so this has to stay
227-
// \\w are just "words" consisting of [a-zA-Z0-9_]
228-
if (!Pattern.compile("[\\w-]+").matcher(resolvedSnapshotname).matches())
232+
// Allowed characters follow the AWS S3 "Safe characters" set documented at
233+
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html#object-key-guidelines :
234+
// 0-9 a-z A-Z ! - _ . * ' ( )
235+
// The path separator '/' is intentionally excluded,
236+
// which is what blocks traversal attempts such as "../../mysnapshot"
237+
if (!SAFE_SNAPSHOT_NAME.matcher(resolvedSnapshotname).matches())
229238
{
230239
throw new IllegalArgumentException("Snapshot name contains illegal characters: " + resolvedSnapshotname);
231240
}
241+
242+
// Because '.' is part of the safe set, the names "." and ".." would pass the charset check
243+
// but resolve to the snapshots/ directory itself or its parent (the live table directory)
244+
// when used as a path component. Reject them explicitly.
245+
if (resolvedSnapshotname.equals(".") || resolvedSnapshotname.equals(".."))
246+
{
247+
throw new IllegalArgumentException("Snapshot name '" + resolvedSnapshotname + "' is reserved");
248+
}
232249
}
233250

234251
private void validateTTL(boolean ephemeral, DurationSpec.IntSecondsBound ttl)
@@ -243,7 +260,6 @@ private void validateTTL(boolean ephemeral, DurationSpec.IntSecondsBound ttl)
243260
if (ephemeral && ttl != null)
244261
throw new IllegalStateException(format("can not take ephemeral snapshot (%s) while ttl is specified too", tag));
245262
}
246-
247263
}
248264

249265
@Override

test/unit/org/apache/cassandra/service/snapshot/SnapshotOptionsTest.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,26 @@ public class SnapshotOptionsTest
3838
@Test
3939
public void testSnapshotNameValidation()
4040
{
41+
// Previously-allowed alphanumerics, '-' and '_' must still be accepted.
4142
validate("atag", USER);
4243
validate("a-tag", USER);
4344
validate("a_tag", USER);
4445
validate("a_tag" + Instant.now().toEpochMilli(), USER);
4546
validate("a_tag_1and_something2-more", USER);
4647
validate("a".repeat(255), USER);
4748

49+
// AWS S3 "Safe characters" newly accepted by the relaxed allowlist:
50+
// ! . * ' ( )
51+
// See https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html#object-key-guidelines
52+
validate("snap.2026-05-20", USER);
53+
validate("important!", USER);
54+
validate("backup*", USER);
55+
validate("o'snap", USER);
56+
validate("snap(1)", USER);
57+
validate("!._-*'()", USER);
58+
// Dots embedded in a name are not traversal: with '/' excluded, "a..tag" is just a literal directory.
59+
validate("a..tag", USER);
60+
4861
assertThatThrownBy(() -> validate("a".repeat(256), USER))
4962
.isInstanceOf(IllegalArgumentException.class)
5063
.hasMessage("Snapshot name must not be more than 255 characters long for " +
@@ -55,13 +68,28 @@ public void testSnapshotNameValidation()
5568
.isInstanceOf(IllegalArgumentException.class)
5669
.hasMessageContaining("Snapshot name must not be more than 255 characters long");
5770

71+
// '/' is not in the S3-safe set; this is what kills traversal attempts like "../../mysnapshot".
5872
assertThatThrownBy(() -> validate('a' + File.pathSeparator() + "tag", USER))
5973
.isInstanceOf(IllegalArgumentException.class)
6074
.hasMessage("Snapshot name contains illegal characters: a/tag");
6175

62-
assertThatThrownBy(() -> validate("a..tag", USER))
76+
// Other characters outside the S3-safe set must still be rejected.
77+
assertThatThrownBy(() -> validate("a tag", USER))
78+
.isInstanceOf(IllegalArgumentException.class)
79+
.hasMessage("Snapshot name contains illegal characters: a tag");
80+
assertThatThrownBy(() -> validate("a:tag", USER))
81+
.isInstanceOf(IllegalArgumentException.class)
82+
.hasMessage("Snapshot name contains illegal characters: a:tag");
83+
84+
// "." and ".." pass the charset check but resolve to the snapshots/ dir itself
85+
// and its parent (the live table dir) respectively, so they must be rejected as reserved.
86+
assertThatThrownBy(() -> validate(".", USER))
87+
.isInstanceOf(IllegalArgumentException.class)
88+
.hasMessage("Snapshot name '.' is reserved");
89+
90+
assertThatThrownBy(() -> validate("..", USER))
6391
.isInstanceOf(IllegalArgumentException.class)
64-
.hasMessage("Snapshot name contains illegal characters: a..tag");
92+
.hasMessage("Snapshot name '..' is reserved");
6593
}
6694

6795
private void validate(String tag, SnapshotType type)

0 commit comments

Comments
 (0)