Skip to content

Commit 0b21d4b

Browse files
chore(dafny): add length check validation for beacon key condition expressions (#2348)
Co-authored-by: Lucas McDonald <lucasmcdonald3@gmail.com>
1 parent 1520838 commit 0b21d4b

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

DynamoDbEncryption/dafny/DynamoDbEncryption/src/DDBSupport.dfy

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,34 @@ module DynamoDBSupport {
3232
import SET = AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes
3333
import NN = DynamoDbNormalizeNumber
3434

35+
// Maximum length, in characters, of any single DynamoDB expression string
36+
// (KeyConditionExpression, FilterExpression, ConditionExpression, etc.) that
37+
// this library will parse. Matches DynamoDB's documented 4 KB server-side
38+
// expression-string limit and bounds the work done by the client-side
39+
// expression parser, preventing unbounded resource consumption from
40+
// pathologically long inputs.
41+
const MAX_EXPRESSION_LENGTH : nat := 4096
42+
43+
// Reject expression strings longer than MAX_EXPRESSION_LENGTH before they
44+
// reach the parser. `name` is the human-readable field name used in the
45+
// error message (e.g. "KeyConditionExpression").
46+
function method ValidateExpressionLength(expr : Option<string>, name : string)
47+
: Result<bool, Error>
48+
{
49+
if expr.None? then
50+
Success(true)
51+
else if |expr.value| <= MAX_EXPRESSION_LENGTH then
52+
Success(true)
53+
else
54+
Failure(E(name + " exceeds maximum length of "
55+
+ String.Base10Int2String(MAX_EXPRESSION_LENGTH as int)
56+
+ " characters."))
57+
}
58+
3559
method GetNumberOfQueries(search : SearchableEncryptionInfo.BeaconVersion, query : DDB.QueryInput)
3660
returns (output : Result<PartitionCount, Error>)
3761
{
62+
var _ :- ValidateExpressionLength(query.KeyConditionExpression, "KeyConditionExpression");
3863
var numberOfQueries :- Filter.GetNumQueries(
3964
search,
4065
query.KeyConditionExpression,
@@ -281,6 +306,8 @@ module DynamoDBSupport {
281306
returns (output : Result<(Option<DDB.ExpressionAttributeValueMap>, PartitionNumber), Error>)
282307
ensures output.Success? ==> output.value.1 < search.numPartitions
283308
{
309+
var _ :- ValidateExpressionLength(keyExpr, "KeyConditionExpression");
310+
var _ :- ValidateExpressionLength(filterExpr, "FilterExpression");
284311
if search.numPartitions <= 1 {
285312
:- Need(values.None? || PartitionName !in values.value, E("If no partitions are configured, do not specify " + PartitionName));
286313
return Success((values, 0));
@@ -309,6 +336,8 @@ module DynamoDBSupport {
309336
returns (output : Result<DDB.QueryInput, Error>)
310337
modifies if search.Some? then search.value.Modifies() else {}
311338
{
339+
var _ :- ValidateExpressionLength(req.KeyConditionExpression, "KeyConditionExpression");
340+
var _ :- ValidateExpressionLength(req.FilterExpression, "FilterExpression");
312341
if search.None? {
313342
var _ :- Filter.TestBeaconize(
314343
actions,
@@ -343,6 +372,8 @@ module DynamoDBSupport {
343372
ensures output.Success? ==> output.value.Items.Some?
344373
modifies if search.Some? then search.value.Modifies() else {}
345374
{
375+
var _ :- ValidateExpressionLength(req.KeyConditionExpression, "KeyConditionExpression");
376+
var _ :- ValidateExpressionLength(req.FilterExpression, "FilterExpression");
346377
if search.None? {
347378
var trimmedItems := Seq.Map(i => DoRemoveBeacons(i), resp.Items.value);
348379
return Success(resp.(Items := Some(trimmedItems)));
@@ -386,6 +417,7 @@ module DynamoDBSupport {
386417
returns (output : Result<DDB.ScanInput, Error>)
387418
modifies if search.Some? then search.value.Modifies() else {}
388419
{
420+
var _ :- ValidateExpressionLength(req.FilterExpression, "FilterExpression");
389421
if search.None? {
390422
var _ :- Filter.TestBeaconize(
391423
actions,
@@ -414,6 +446,7 @@ module DynamoDBSupport {
414446
ensures ret.Success? ==> ret.value.Items.Some?
415447
modifies if search.Some? then search.value.Modifies() else {}
416448
{
449+
var _ :- ValidateExpressionLength(req.FilterExpression, "FilterExpression");
417450
if search.None? {
418451
var trimmedItems := Seq.Map(i => DoRemoveBeacons(i), resp.Items.value);
419452
return Success(resp.(Items := Some(trimmedItems)));

DynamoDbEncryption/dafny/DynamoDbEncryption/test/DDBSupport.dfy

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,58 @@ module TestDDBSupport {
8888
);
8989
result :- expect QueryInputForBeacons(Some(search), FullTableConfig.attributeActionsOnEncrypt, queryInput);
9090
}
91+
92+
// ValidateExpressionLength is the chokepoint that prevents
93+
// pathologically long DynamoDB expression strings from reaching the
94+
// recursive expression parser. Verify each branch of the helper.
95+
method {:test} TestValidateExpressionLength() {
96+
// None: nothing to validate.
97+
expect ValidateExpressionLength(None, "FilterExpression") == Success(true);
98+
99+
// Empty string: trivially within the limit.
100+
expect ValidateExpressionLength(Some(""), "FilterExpression") == Success(true);
101+
102+
// Short string: ordinary case.
103+
expect ValidateExpressionLength(Some("a < :b"), "FilterExpression") == Success(true);
104+
105+
// Exactly at the limit (4096 characters): accepted.
106+
var atLimit : string := seq(MAX_EXPRESSION_LENGTH, _ => ' ');
107+
expect |atLimit| == MAX_EXPRESSION_LENGTH;
108+
expect ValidateExpressionLength(Some(atLimit), "KeyConditionExpression") == Success(true);
109+
110+
// One over the limit (4097 characters): rejected with the expected
111+
// error, which names the field that violated the constraint.
112+
var overLimit : string := seq(MAX_EXPRESSION_LENGTH + 1, _ => ' ');
113+
expect |overLimit| == MAX_EXPRESSION_LENGTH + 1;
114+
expect ValidateExpressionLength(Some(overLimit), "KeyConditionExpression")
115+
== Failure(E("KeyConditionExpression exceeds maximum length of 4096 characters."));
116+
expect ValidateExpressionLength(Some(overLimit), "FilterExpression")
117+
== Failure(E("FilterExpression exceeds maximum length of 4096 characters."));
118+
}
119+
120+
// Confirm that the length check is wired into the public API surface:
121+
// an over-long KeyConditionExpression must be rejected by
122+
// QueryInputForBeacons before any expression parsing takes place.
123+
// search := None is sufficient — the length check runs first,
124+
// independent of whether searchable encryption is configured.
125+
method {:test} TestQueryInputForBeaconsRejectsLongKeyExpression() {
126+
var overLimit : string := seq(MAX_EXPRESSION_LENGTH + 1, _ => ' ');
127+
var queryInput := DDB.QueryInput(
128+
TableName := "SomeTable",
129+
KeyConditionExpression := Some(overLimit)
130+
);
131+
var result := QueryInputForBeacons(None, map[], queryInput);
132+
expect result == Failure(E("KeyConditionExpression exceeds maximum length of 4096 characters."));
133+
}
134+
135+
// Same as above for FilterExpression on the scan path.
136+
method {:test} TestScanInputForBeaconsRejectsLongFilterExpression() {
137+
var overLimit : string := seq(MAX_EXPRESSION_LENGTH + 1, _ => ' ');
138+
var scanInput := DDB.ScanInput(
139+
TableName := "SomeTable",
140+
FilterExpression := Some(overLimit)
141+
);
142+
var result := ScanInputForBeacons(None, map[], scanInput);
143+
expect result == Failure(E("FilterExpression exceeds maximum length of 4096 characters."));
144+
}
91145
}

0 commit comments

Comments
 (0)