@@ -39,7 +39,7 @@ use std::collections::BTreeMap;
3939use dpp:: data_contract:: accessors:: v0:: DataContractV0Getters ;
4040use dpp:: data_contract:: document_type:: methods:: { DocumentTypeBasicMethods , DocumentTypeV0Methods } ;
4141use dpp:: data_contract:: document_type:: accessors:: DocumentTypeV0Getters ;
42- use dpp:: data_contract:: document_type:: DocumentTypeRef ;
42+ use dpp:: data_contract:: document_type:: { DocumentPropertyType , DocumentTypeRef } ;
4343use dpp:: data_contract:: DataContract ;
4444use dpp:: platform_value:: Value ;
4545use dpp:: state_transition:: batch_transition:: batched_transition:: document_transition:: DocumentTransitionV0Methods ;
@@ -814,15 +814,32 @@ fn canonical_value_for_key(
814814 value : & Value ,
815815 platform_version : & PlatformVersion ,
816816) -> Option < Value > {
817- if let Some ( property) = document_type. flattened_properties ( ) . get ( key) {
818- let encoded = property
819- . property_type
820- . encode_value_for_tree_keys ( value)
821- . ok ( ) ?;
822- return property
823- . property_type
824- . decode_value_for_tree_keys ( & encoded)
825- . ok ( ) ;
817+ let property_type = document_type
818+ . flattened_properties ( )
819+ . get ( key)
820+ . map ( |property| & property. property_type ) ;
821+ // Base58 decoding takes time quadratic in its input, and a filter's operands come from
822+ // unauthenticated requests: refuse identifier text longer than any 32-byte identifier.
823+ let identifier = matches ! ( key, "$id" | "$ownerId" | "$creatorId" | "$moderatedBy" )
824+ || matches ! (
825+ property_type,
826+ Some (
827+ DocumentPropertyType :: Identifier | DocumentPropertyType :: IdentifierWithReference ( _)
828+ )
829+ ) ;
830+ if identifier && matches ! ( value, Value :: Text ( text) if text. len( ) > MAX_BASE58_IDENTIFIER_LEN ) {
831+ return None ;
832+ }
833+ if let Some ( property_type) = property_type {
834+ let encoded = property_type. encode_value_for_tree_keys ( value) . ok ( ) ?;
835+ // The index-key form of an empty byte array is empty, which decodes as null.
836+ if encoded. is_empty ( ) && !value. is_null ( ) {
837+ return match property_type {
838+ DocumentPropertyType :: ByteArray ( _) => Some ( Value :: Bytes ( vec ! [ ] ) ) ,
839+ _ => None ,
840+ } ;
841+ }
842+ return property_type. decode_value_for_tree_keys ( & encoded) . ok ( ) ;
826843 }
827844 let serialized = document_type
828845 . serialize_value_for_key ( key, value, platform_version)
@@ -832,6 +849,10 @@ fn canonical_value_for_key(
832849 . ok ( )
833850}
834851
852+ /// The longest base58 text of a 32-byte identifier.
853+ #[ cfg( any( feature = "server" , feature = "verify" ) ) ]
854+ const MAX_BASE58_IDENTIFIER_LEN : usize = 44 ;
855+
835856/// Canonicalize a clause operand with `canonical`: element-wise for the list operands of
836857/// `IN` and `BETWEEN*`, directly otherwise.
837858#[ cfg( any( feature = "server" , feature = "verify" ) ) ]
@@ -3341,5 +3362,95 @@ mod tests {
33413362 TransitionCheckResult :: Pass
33423363 ) ;
33433364 }
3365+
3366+ #[ test]
3367+ fn should_refuse_identifier_text_longer_than_any_identifier ( ) {
3368+ let contract = contract ( ) ;
3369+ for field in [ "identifierField" , "$id" ] {
3370+ let clauses = if field == "$id" {
3371+ InternalClauses {
3372+ primary_key_equal_clause : Some ( WhereClause {
3373+ field : field. to_string ( ) ,
3374+ operator : WhereOperator :: Equal ,
3375+ value : Value :: Text ( "z" . repeat ( 120_000 ) ) ,
3376+ } ) ,
3377+ ..Default :: default ( )
3378+ }
3379+ } else {
3380+ equal ( field, Value :: Text ( "z" . repeat ( 120_000 ) ) )
3381+ } ;
3382+ let mut filter = DriveDocumentQueryFilter {
3383+ contract : & contract,
3384+ document_type_name : "withByteArrays" . to_string ( ) ,
3385+ action_clauses : DocumentActionMatchClauses :: Delete {
3386+ original_document_clauses : clauses,
3387+ } ,
3388+ } ;
3389+ assert ! (
3390+ filter
3391+ . canonicalize_clause_values( PlatformVersion :: latest( ) )
3392+ . is_err( ) ,
3393+ "{field}"
3394+ ) ;
3395+ }
3396+ }
3397+
3398+ #[ test]
3399+ fn should_keep_empty_byte_array_operands ( ) {
3400+ let contract = contract ( ) ;
3401+ let platform_version = PlatformVersion :: latest ( ) ;
3402+ for ( operator, value) in [
3403+ ( WhereOperator :: Equal , Value :: Bytes ( vec ! [ ] ) ) ,
3404+ (
3405+ WhereOperator :: In ,
3406+ Value :: Array ( vec ! [ Value :: Bytes ( vec![ ] ) , Value :: Bytes ( vec![ 1 ] ) ] ) ,
3407+ ) ,
3408+ ] {
3409+ let mut filter = DriveDocumentQueryFilter {
3410+ contract : & contract,
3411+ document_type_name : "withByteArrays" . to_string ( ) ,
3412+ action_clauses : DocumentActionMatchClauses :: Create {
3413+ new_document_clauses : InternalClauses {
3414+ equal_clauses : if operator == WhereOperator :: Equal {
3415+ BTreeMap :: from ( [ (
3416+ "byteArrayField" . to_string ( ) ,
3417+ WhereClause {
3418+ field : "byteArrayField" . to_string ( ) ,
3419+ operator,
3420+ value : value. clone ( ) ,
3421+ } ,
3422+ ) ] )
3423+ } else {
3424+ BTreeMap :: new ( )
3425+ } ,
3426+ in_clauses : if operator == WhereOperator :: In {
3427+ vec ! [ WhereClause {
3428+ field: "byteArrayField" . to_string( ) ,
3429+ operator,
3430+ value: value. clone( ) ,
3431+ } ]
3432+ } else {
3433+ vec ! [ ]
3434+ } ,
3435+ ..Default :: default ( )
3436+ } ,
3437+ } ,
3438+ } ;
3439+ filter
3440+ . canonicalize_clause_values ( platform_version)
3441+ . expect ( "an empty byte array is a byte array" ) ;
3442+ assert ! ( filter. validate( ) . is_valid( ) , "{operator:?}" ) ;
3443+ let transition = create (
3444+ & contract,
3445+ "withByteArrays" ,
3446+ BTreeMap :: from ( [ ( "byteArrayField" . to_string ( ) , Value :: Bytes ( vec ! [ ] ) ) ] ) ,
3447+ ) ;
3448+ assert_eq ! (
3449+ filter. matches_document_transition( & transition, None , platform_version) ,
3450+ TransitionCheckResult :: Pass ,
3451+ "{operator:?}"
3452+ ) ;
3453+ }
3454+ }
33443455 }
33453456}
0 commit comments