Skip to content

Commit 1520838

Browse files
fix: validate attribute name length by UTF-8 byte count (#2329)
1 parent 598c80b commit 1520838

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

DynamoDbEncryption/dafny/DynamoDbEncryption/src/DynamoToStruct.dfy

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,10 @@ module DynamoToStruct {
282282
const LENGTH_LEN64 : uint64 := 4 // number of bytes in an encoded count or length
283283
const PREFIX_LEN64 : uint64 := 6 // number of bytes in a prefix, i.e. 2-byte type and 4-byte length
284284

285+
// Max bytes in a DynamoDB attribute name (UTF-8). Validated byte-based, not via the
286+
// character-based IsValid_AttributeName, to match the real DynamoDB rule.
287+
const MAX_ATTR_NAME_LEN64 : uint64 := 65535
288+
285289
function method AttrToTypeId(a : AttributeValue) : TerminalTypeId
286290
{
287291
match a {
@@ -946,6 +950,8 @@ module DynamoToStruct {
946950
var name :- UTF8.Encode(key);
947951
assert UTF8.Decode(name).Success?;
948952
SequenceIsSafeBecauseItIsInMemory(name);
953+
// Attribute name length is validated on the UTF-8 byte length, not the character count.
954+
:- Need(1 <= |name| as uint64 <= MAX_ATTR_NAME_LEN64, "Attribute name must be between 1 and 65535 UTF-8 bytes");
949955
var len :- U32ToBigEndian64(|name| as uint64);
950956

951957
//= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#key-value-pair-entries
@@ -1263,6 +1269,8 @@ module DynamoToStruct {
12631269
var len : uint64 :- BigEndianPosToU32As64(serialized, pos);
12641270
var pos := pos + LENGTH_LEN64;
12651271
:- Need(len <= serialized_size-pos, "Key of Map of Structured Data has too few bytes");
1272+
// Attribute name length is validated on the UTF-8 byte length, not the character count.
1273+
:- Need(1 <= len <= MAX_ATTR_NAME_LEN64, "Attribute name must be between 1 and 65535 UTF-8 bytes");
12661274
var key :- UTF8.Decode(serialized[pos..pos+len]);
12671275
var pos := pos + len;
12681276

DynamoDbEncryption/dafny/DynamoDbEncryption/test/DynamoToStruct.dfy

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,4 +568,32 @@ module DynamoToStructTest {
568568
expect struct.Failure?;
569569
expect struct.error == E("Depth of attribute structure to serialize exceeds limit of 32");
570570
}
571+
572+
method {:test} TestAttributeNameTooLongBytes() {
573+
// 0xE9 is a 2-byte UTF-8 char, so 40000 chars => 80000 bytes, exceeding the 65535-byte limit.
574+
var twoByteChar := 0xE9 as char;
575+
var longKey : string := seq(40000, i => twoByteChar);
576+
var mapValue := AttributeValue.M(map[longKey := AttributeValue.BOOL(true)]);
577+
var result := AttrToStructured(mapValue);
578+
if !result.Failure? {
579+
print "\nAttrToStructured should have failed for an over-long attribute name\n";
580+
}
581+
expect result.Failure?;
582+
expect result.error == "Attribute name must be between 1 and 65535 UTF-8 bytes";
583+
}
584+
585+
method {:test} TestDecodeAttributeNameTooLongBytes() {
586+
// Decode side: a serialized map whose key length field is 65536 bytes (one over the
587+
// limit), with that many key bytes actually present so the length check is what fails.
588+
var bigKey : seq<uint8> := seq(65536, i => 0x41 as uint8);
589+
// count=1 | key type id STRING (0,1) | key length 65536 (0x00010000) | key bytes | value type STRING (0,1) | value length 0
590+
var mapBytes : seq<uint8> := [0, 0, 0, 1] + [0, 1] + [0, 1, 0, 0] + bigKey + [0, 1] + [0, 0, 0, 0];
591+
var data := StructuredDataTerminal(value := mapBytes, typeId := SE.MAP);
592+
var result := StructuredToAttr(data);
593+
if !result.Failure? {
594+
print "\nStructuredToAttr should have failed for an over-long attribute name\n";
595+
}
596+
expect result.Failure?;
597+
expect result.error == "Attribute name must be between 1 and 65535 UTF-8 bytes";
598+
}
571599
}

DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/test/PutItemTransform.dfy

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,33 @@ module PutItemTransformTest {
3030

3131
const BasicItem : DDB.AttributeMap := map["bar" := DDB.AttributeValue.S("baz")]
3232

33+
method {:test} TestPutItemAttributeNameTooLongBytes() {
34+
var middlewareUnderTest := TestFixtures.GetDynamoDbEncryptionTransforms();
35+
var tableName := GetTableName("foo");
36+
// 0xE9 is a 2-byte UTF-8 char, so 40000 chars => 80000 bytes, exceeding the 65535-byte limit.
37+
var twoByteChar := 0xE9 as char;
38+
var longKey : DDB.AttributeName := seq(40000, i => twoByteChar);
39+
var input := DDB.PutItemInput(
40+
TableName := tableName,
41+
Item := map[
42+
"bar" := DDB.AttributeValue.S("key"),
43+
"encrypt" := DDB.AttributeValue.M(map[longKey := DDB.AttributeValue.S("x")])
44+
]
45+
);
46+
var transformed := middlewareUnderTest.PutItemInputTransform(
47+
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.PutItemInputTransformInput(
48+
sdkInput := input
49+
)
50+
);
51+
expect transformed.Failure?;
52+
expect transformed.error.AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor?;
53+
var encErr := transformed.error.AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor;
54+
expect encErr.AwsCryptographyDbEncryptionSdkDynamoDb?;
55+
var ddbErr := encErr.AwsCryptographyDbEncryptionSdkDynamoDb;
56+
expect ddbErr.DynamoDbEncryptionException?;
57+
expect ddbErr.message == "Attribute name must be between 1 and 65535 UTF-8 bytes";
58+
}
59+
3360
method TestPutItemInputMultiFail(plaintextOverride : Option<AwsCryptographyDbEncryptionSdkDynamoDbTypes.PlaintextOverride>) {
3461
assume {:axiom} false;
3562
var middlewareUnderTest := TestFixtures.GetDynamoDbEncryptionTransformsMulti(plaintextOverride);

0 commit comments

Comments
 (0)