-
Notifications
You must be signed in to change notification settings - Fork 3.9k
CASSANDRA-21389 trunk Harden snapshot names on server side #4826
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
| import java.util.Collections; | ||
| import java.util.Map; | ||
| import java.util.function.Predicate; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| import com.google.common.util.concurrent.RateLimiter; | ||
|
|
||
|
|
@@ -33,13 +34,23 @@ | |
| import org.apache.cassandra.config.DurationSpec; | ||
| import org.apache.cassandra.db.ColumnFamilyStore; | ||
| import org.apache.cassandra.io.sstable.format.SSTableReader; | ||
| import org.apache.cassandra.io.util.File; | ||
| import org.apache.cassandra.schema.SchemaConstants; | ||
|
|
||
| import static java.lang.String.format; | ||
| import static org.apache.cassandra.schema.SchemaConstants.FILENAME_LENGTH; | ||
| import static org.apache.cassandra.utils.FBUtilities.now; | ||
|
|
||
| public class SnapshotOptions | ||
| { | ||
| public static final String SKIP_FLUSH = "skipFlush"; | ||
| public static final String TTL = "ttl"; | ||
|
|
||
| // Conservative subset of the AWS S3 "Safe characters" set: 0-9 a-z A-Z - _ . | ||
| // See the validation site in validateTag for the full rationale on excluded characters. | ||
| // Hyphen is placed last in the character class, so it stays literal and never becomes a range operator. | ||
| private static final Pattern SAFE_SNAPSHOT_NAME = Pattern.compile("[a-zA-Z0-9_.-]+"); | ||
|
|
||
| public final SnapshotType type; | ||
| public final String tag; | ||
| public final DurationSpec.IntSecondsBound ttl; | ||
|
|
@@ -88,7 +99,7 @@ public static SnapshotOptions userSnapshot(String tag, Map<String, String> optio | |
| return builder.build(); | ||
| } | ||
|
|
||
| public String getSnapshotName(Instant creationTime) | ||
| public static String getSnapshotName(SnapshotType type, String tag, Instant creationTime) | ||
| { | ||
| // Diagnostic snapshots have very specific naming convention hence we are keeping it. | ||
| // Repair snapshots rely on snapshots having name of their repair session ids | ||
|
|
@@ -189,10 +200,66 @@ public Builder rateLimiter(RateLimiter rateLimiter) | |
| } | ||
|
|
||
| public SnapshotOptions build() | ||
| { | ||
| validateTag(tag); | ||
| validateTTL(ephemeral, ttl); | ||
|
|
||
| if (rateLimiter == null) | ||
| rateLimiter = DatabaseDescriptor.getSnapshotRateLimiter(); | ||
|
|
||
| return new SnapshotOptions(this); | ||
| } | ||
|
|
||
| private void validateTag(String tag) | ||
| { | ||
| if (tag == null || tag.isEmpty()) | ||
| throw new RuntimeException("You must supply a snapshot name."); | ||
| throw new IllegalArgumentException("You must supply a snapshot name."); | ||
|
|
||
| if (tag.contains(File.pathSeparator())) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we may need to reject both the native path separator and '/' since on windows '\' is the path separator but '/' is still treated as a path separator, so in theory the path traversal attack remains on windows via that means.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually potentially this is fine it seems we've remove windows support as of:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we do not support Windows |
||
| { | ||
| throw new IllegalArgumentException("Snapshot name cannot contain " + File.pathSeparator()); | ||
| } | ||
|
|
||
| if (tag.equals(".") || tag.equals("..")) | ||
| { | ||
| throw new IllegalArgumentException("Snapshot name '" + tag + "' is reserved"); | ||
| } | ||
|
|
||
| if (!CassandraRelevantProperties.SNAPSHOT_NAME_VALIDATION.getBoolean()) | ||
| return; | ||
|
|
||
| // Pre-generate snapshot name for the sake of the validation. | ||
| // getSnapshotName logic does not return raw "tag" as snapshot name every time, | ||
| // it e.g. prepends timestamp and type for system snapshots, and we need to validate it as a whole. | ||
| // If, for example, tag would be less than max allowed FILENAME_LENGTH, | ||
| // we might in fact produce a snapshot name longer than FILENAME_LENGTH if we prepended a timestamp to it. | ||
| String resolvedSnapshotName = SnapshotOptions.getSnapshotName(type, tag, now()); | ||
|
|
||
| // the length of valid snapshot name has to be less than or equal to FILENAME_LEGTH - that is 255 - | ||
| // we are following the max length as it is in SchemaConstants for table name. | ||
| if (resolvedSnapshotName.length() > SchemaConstants.FILENAME_LENGTH) | ||
| { | ||
| throw new IllegalArgumentException(format("Snapshot name must not be more than %d characters long for " + | ||
| "resolved snapshot name (got %d characters for \"%s\")", | ||
| FILENAME_LENGTH, resolvedSnapshotName.length(), resolvedSnapshotName)); | ||
| } | ||
|
|
||
| // Allowed characters are a conservative subset of the AWS S3 "Safe characters" set | ||
| // (https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html#object-key-guidelines): | ||
| // 0-9 a-z A-Z - _ . | ||
| // The remaining S3-safe characters (! * ' ( )) are intentionally excluded as they are | ||
|
smiklosovic marked this conversation as resolved.
|
||
| // shell-significant and error-prone in paths, and the path separator '/' is excluded too, | ||
| // which is what blocks traversal attempts such as "../../mysnapshot" | ||
| if (type == SnapshotType.USER && !SAFE_SNAPSHOT_NAME.matcher(resolvedSnapshotName).matches()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Admittedly USER is the only directly user originated input. |
||
| { | ||
| throw new IllegalArgumentException("Snapshot name contains illegal characters: " + resolvedSnapshotName + ". " + | ||
| "Allowed characters must match the pattern: " + SAFE_SNAPSHOT_NAME.pattern() + | ||
| " with a maximum of length of " + FILENAME_LENGTH + " characters."); | ||
| } | ||
| } | ||
|
|
||
| private void validateTTL(boolean ephemeral, DurationSpec.IntSecondsBound ttl) | ||
| { | ||
| if (ttl != null) | ||
| { | ||
| int minAllowedTtlSecs = CassandraRelevantProperties.SNAPSHOT_MIN_ALLOWED_TTL_SECONDS.getInt(); | ||
|
|
@@ -202,11 +269,6 @@ public SnapshotOptions build() | |
|
|
||
| if (ephemeral && ttl != null) | ||
| throw new IllegalStateException(format("can not take ephemeral snapshot (%s) while ttl is specified too", tag)); | ||
|
|
||
| if (rateLimiter == null) | ||
| rateLimiter = DatabaseDescriptor.getSnapshotRateLimiter(); | ||
|
|
||
| return new SnapshotOptions(this); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Part of me is wondering if we allow folks to configure the check off, maybe it may also make sense to allow them to configure what to restrict on?
Then if they encounter some character that they do actually validly need or can't change for some reason, operators can still benefit from some restrictions.
Debatable though maybe adds more configuration/reasoning complexity where we just want simplicity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we are complicating it too much imho with allowing them to do that, I think that is just premature as of now, they have a way to just completely remove the validation, that is good enough imho