RGW S3 listing operations provided a way for authenticated users to
cause a denial of service against OMAPs holding bucket indices.
Bound the min & max values that a user could pass into the max-X
parameters, to keep the system safe. The default of 1000 is chosen to
match AWS S3 behavior.
Affected operations:
- ListBucket, via max-keys
- ListBucketVersions, via max-keys
- ListBucketMultiPartUploads, via max-uploads
- ListMultipartUploadParts, via max-parts
The Swift bucket listing codepath already enforced a limit, so is
unaffected by this issue.
Prior to this commit, the effective limit is the lower of
osd_max_omap_entries_per_request or osd_max_omap_bytes_per_request.
Backport: luminous, mimic
Fixes: http://tracker.ceph.com/issues/35994
Signed-off-by: Robin H. Johnson <rjohnson@digitalocean.com>
(cherry picked from commit
d79f68a1e31f4bc917eec1b6bbc8e8446377dc6b)
Conflicts:
src/common/options.cc:
Conflicts due to options from master
"of RGW instances under heavy use. If you would like "
"to turn off cache expiry, set this value to zero."),
+ Option("rgw_max_listing_results", Option::TYPE_UINT,
+ Option::LEVEL_ADVANCED)
+ .set_default(1000)
+ .set_min_max(1, 100000)
+ .add_service("rgw")
+ .set_description("Upper bound on results in listing operations, ListBucket max-keys"),
+ .set_long_description("This caps the maximum permitted value for listing-like operations in RGW S3. "
+ "Affects ListBucket(max-keys), "
+ "ListBucketVersions(max-keys), "
+ "ListBucketMultiPartUploads(max-uploads), "
+ "ListMultipartUploadParts(max-parts)"),
});
}
int RGWListBucket::parse_max_keys()
{
- if (!max_keys.empty()) {
- char *endptr;
- max = strtol(max_keys.c_str(), &endptr, 10);
- if (endptr) {
- if (endptr == max_keys.c_str()) return -EINVAL;
- while (*endptr && isspace(*endptr)) // ignore white space
- endptr++;
- if (*endptr) {
- return -EINVAL;
- }
- }
- } else {
- max = default_max;
- }
-
- return 0;
+ // Bound max value of max-keys to configured value for security
+ // Bound min value of max-keys to '0'
+ // Some S3 clients explicitly send max-keys=0 to detect if the bucket is
+ // empty without listing any items.
+ op_ret = parse_value_and_bound(max_keys, &max, 0, g_conf()->rgw_max_listing_results, default_max);
}
void RGWListBucket::pre_exec()
virtual const string name() { return "get_cluster_stat"; }
};
+static inline int parse_value_and_bound(const string &input, long *output, const long lower_bound, const long upper_bound, const long default_val)
+{
+ if (!input.empty()) {
+ char *endptr;
+ *output = strtol(input.c_str(), &endptr, 10);
+ if (endptr) {
+ if (endptr == input.c_str()) return -EINVAL;
+ while (*endptr && isspace(*endptr)) // ignore white space
+ endptr++;
+ if (*endptr) {
+ return -EINVAL;
+ }
+ }
+ if(*output > upper_bound) {
+ *output = upper_bound;
+ }
+ if(*output < lower_bound) {
+ *output = lower_bound;
+ }
+ } else {
+ *output = default_val;
+ }
+
+ return 0;
+}
#endif /* CEPH_RGW_OP_H */
}
string str = s->info.args.get("max-parts");
- if (!str.empty())
- max_parts = atoi(str.c_str());
+ op_ret = parse_value_and_bound(str, &max_parts, 0, g_conf()->rgw_max_listing_results, max_parts);
return op_ret;
}
delimiter = s->info.args.get("delimiter");
prefix = s->info.args.get("prefix");
string str = s->info.args.get("max-uploads");
- if (!str.empty())
- max_uploads = atoi(str.c_str());
- else
- max_uploads = default_max;
+ op_ret = parse_value_and_bound(str, &max_uploads, 0, g_conf()->rgw_max_listing_results, default_max);
+ if (op_ret < 0) {
+ return op_ret;
+ }
string key_marker = s->info.args.get("key-marker");
string upload_id_marker = s->info.args.get("upload-id-marker");
if (op_ret < 0) {
return op_ret;
}
+ // S3 behavior is to silently cap the max-keys.
+ // Swift behavior is to abort.
if (max > default_max)
return -ERR_PRECONDITION_FAILED;