CVE-2020-1898 (published 2021-03-11T01:15Z)

The fb_unserialize function did not impose a depth limit for nested deserialization. That meant a maliciously constructed string could cause deserialization to recurse, leading to stack exhaustion. This issue affected HHVM prior to v4.32.3, between versions 4.33.0 and 4.56.0, 4.57.0, 4.58.0, 4.58.1, 4.59.0, 4.60.0, 4.61.0, 4.62.0.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:N/A:P)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-674
cwe_name: Uncontrolled Recursion
description: The product does not properly control the amount of recursion that takes place,  consuming excessive resources, such as allocated memory or the program stack.
extended_description: 
        
      
      

Fix information:
repo_url: https://github.com/facebook/hhvm
commit_details: repo_url: https://github.com/facebook/hhvm
author: jjergus
author_date: 2020-06-29 11:36:00-07:00
msg: CVE-2020-1898

Don't allow for unlimited nesting in FBUnserialize or fb_compact_unserialize
repository: repo_url: https://github.com/facebook/hhvm
repo_name: facebook/hhvm
description: A virtual machine for executing programs written in Hack.
repo_language: Hack
owner: facebook
file_changes: filename: builtins_fb.hhi
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,7 @@ const FB_UNSERIALIZE_NONSTRING_VALUE = 0;
 const FB_UNSERIALIZE_UNEXPECTED_END = 0;
 const FB_UNSERIALIZE_UNRECOGNIZED_OBJECT_TYPE = 0;
 const FB_UNSERIALIZE_UNEXPECTED_ARRAY_KEY_TYPE = 0;
+const FB_UNSERIALIZE_MAX_DEPTH_EXCEEDED = 0;
 
 const FB_SERIALIZE_HACK_ARRAYS = 0;
 const FB_SERIALIZE_HACK_ARRAYS_AND_KEYSETS = 0;

programming_language: PHP
filename: FBSerialize-inl.h
change_type: ModificationType.MODIFY
diff: @@ -406,7 +406,7 @@ inline typename V::VariantType FBUnserializer<V>::unserialize(
   folly::StringPiece serialized) {
 
   FBUnserializer<V> unserializer(serialized);
-  return unserializer.unserializeThing();
+  return unserializer.unserializeThing(0);
 }
 
 template <class V>
@@ -524,7 +524,7 @@ inline FBSerializeBase::Code FBUnserializer<V>::nextCode() const {
 }
 
 template <class V>
-inline typename V::MapType FBUnserializer<V>::unserializeMap() {
+inline typename V::MapType FBUnserializer<V>::unserializeMap(size_t depth) {
   p_ += CODE_SIZE;
 
   typename V::MapType ret = V::createMap();
@@ -536,14 +536,14 @@ inline typename V::MapType FBUnserializer<V>::unserializeMap() {
       case FB_SERIALIZE_STRING:
         {
           auto key = unserializeString();
-          auto value = unserializeThing();
+          auto value = unserializeThing(depth + 1);
           V::mapSet(ret, std::move(key), std::move(value));
         }
         break;
       default:
         {
           auto key = unserializeInt64();
-          auto value = unserializeThing();
+          auto value = unserializeThing(depth + 1);
           V::mapSet(ret, std::move(key), std::move(value));
         }
     }
@@ -556,14 +556,15 @@ inline typename V::MapType FBUnserializer<V>::unserializeMap() {
 }
 
 template <class V>
-inline typename V::VectorType FBUnserializer<V>::unserializeVector() {
+inline typename V::VectorType
+FBUnserializer<V>::unserializeVector(size_t depth) {
   p_ += CODE_SIZE;
 
   typename V::VectorType ret = V::createVector();
 
   size_t code = nextCode();
   while (code != FB_SERIALIZE_STOP) {
-    V::vectorAppend(ret, unserializeThing());
+    V::vectorAppend(ret, unserializeThing(depth + 1));
     code = nextCode();
   }
   p_ += CODE_SIZE;
@@ -571,7 +572,7 @@ inline typename V::VectorType FBUnserializer<V>::unserializeVector() {
 }
 
 template <class V>
-inline typename V::VectorType FBUnserializer<V>::unserializeList() {
+inline typename V::VectorType FBUnserializer<V>::unserializeList(size_t depth) {
   p_ += CODE_SIZE;
 
   // the list size is written so we can reserve it in the vector
@@ -582,7 +583,7 @@ inline typename V::VectorType FBUnserializer<V>::unserializeList() {
 
   size_t code = nextCode();
   while (code != FB_SERIALIZE_STOP) {
-    V::vectorAppend(ret, unserializeThing());
+    V::vectorAppend(ret, unserializeThing(depth + 1));
     code = nextCode();
   }
   p_ += CODE_SIZE;
@@ -590,7 +591,7 @@ inline typename V::VectorType FBUnserializer<V>::unserializeList() {
 }
 
 template <class V>
-inline typename V::SetType FBUnserializer<V>::unserializeSet() {
+inline typename V::SetType FBUnserializer<V>::unserializeSet(size_t depth) {
   p_ += CODE_SIZE;
 
   // the set size is written so we can reserve it in the set
@@ -601,7 +602,7 @@ inline typename V::SetType FBUnserializer<V>::unserializeSet() {
 
   size_t code = nextCode();
   while (code != FB_SERIALIZE_STOP) {
-    V::setAppend(ret, unserializeThing());
+    V::setAppend(ret, unserializeThing(depth + 1));
     code = nextCode();
   }
   p_ += CODE_SIZE;
@@ -664,7 +665,12 @@ inline folly::StringPiece FBUnserializer<V>::getSerializedMap() {
 }
 
 template <class V>
-inline typename V::VariantType FBUnserializer<V>::unserializeThing() {
+inline typename V::VariantType
+FBUnserializer<V>::unserializeThing(size_t depth) {
+  if (UNLIKELY(depth > 1024)) {
+    throw UnserializeError("depth > 1024");
+  }
+
   size_t code = nextCode();
 
   switch (code) {
@@ -677,7 +683,7 @@ inline typename V::VariantType FBUnserializer<V>::unserializeThing() {
     case FB_SERIALIZE_STRING:
       return V::fromString(unserializeString());
     case FB_SERIALIZE_STRUCT:
-      return V::fromMap(unserializeMap());
+      return V::fromMap(unserializeMap(depth));
     case FB_SERIALIZE_NULL:
       ++p_;
       return V::createNull();
@@ -686,11 +692,11 @@ inline typename V::VariantType FBUnserializer<V>::unserializeThing() {
     case FB_SERIALIZE_BOOLEAN:
       return V::fromBool(unserializeBoolean());
     case FB_SERIALIZE_VECTOR:
-      return V::fromVector(unserializeVector());
+      return V::fromVector(unserializeVector(depth));
     case FB_SERIALIZE_LIST:
-      return V::fromVector(unserializeList());
+      return V::fromVector(unserializeList(depth));
     case FB_SERIALIZE_SET:
-      return V::fromSet(unserializeSet());
+      return V::fromSet(unserializeSet(depth));
     default:
       throw UnserializeError("Invalid code: " + folly::to<std::string>(code)
                              + " at location " + folly::to<std::string>(p_));

programming_language: C++
filename: FBSerialize.h
change_type: ModificationType.MODIFY
diff: @@ -165,14 +165,14 @@ struct FBUnserializer : private FBSerializeBase {
   double unserializeDouble();
   typename V::StringType unserializeString();
   folly::StringPiece unserializeStringPiece();
-  typename V::MapType unserializeMap();
-  typename V::VectorType unserializeVector();
-  typename V::VectorType unserializeList();
-  typename V::SetType unserializeSet();
+  typename V::MapType unserializeMap(size_t depth);
+  typename V::VectorType unserializeVector(size_t depth);
+  typename V::VectorType unserializeList(size_t depth);
+  typename V::SetType unserializeSet(size_t depth);
   // read the next map but don't unserialze it (for lazy or delay
   // unserialization)
   folly::StringPiece getSerializedMap();
-  typename V::VariantType unserializeThing();
+  typename V::VariantType unserializeThing(size_t depth);
 
   void advance(size_t delta);
   Code nextCode() const;

programming_language: C++
filename: VariantController.h
change_type: ModificationType.MODIFY
diff: @@ -283,12 +283,10 @@ struct VariantControllerImpl {
     return set.size();
   }
   static void setAppend(SetType& set, const VariantType& v) {
-    auto value_type = type(v);
-    if (value_type != HPHP::serialize::Type::INT64 &&
-        value_type != HPHP::serialize::Type::STRING) {
+    if (!v.isInteger() && !v.isString()) {
       throw HPHP::serialize::UnserializeError(
-          "Unsupported keyset element of type " +
-          folly::to<std::string>(value_type));
+        "Keysets can only contain integers or strings"
+      );
     }
     set.append(v);
   }

programming_language: C++
filename: ext_fb.cpp
change_type: ModificationType.MODIFY
diff: @@ -66,6 +66,7 @@ static const UChar32 SUBSTITUTION_CHARACTER = 0xFFFD;
 #define FB_UNSERIALIZE_UNEXPECTED_END            0x0002
 #define FB_UNSERIALIZE_UNRECOGNIZED_OBJECT_TYPE  0x0003
 #define FB_UNSERIALIZE_UNEXPECTED_ARRAY_KEY_TYPE 0x0004
+#define FB_UNSERIALIZE_MAX_DEPTH_EXCEEDED        0x0005
 
 #ifdef FACEBOOK
 # define HHVM_FACEBOOK true
@@ -705,7 +706,10 @@ int fb_compact_unserialize_int64_from_buffer(
 const StaticString s_empty("");
 
 int fb_compact_unserialize_from_buffer(
-  Variant& out, const char* buf, int n, int& p) {
+    Variant& out, const char* buf, int n, int& p, size_t depth) {
+  if (UNLIKELY(depth > 1024)) {
+    return FB_UNSERIALIZE_MAX_DEPTH_EXCEEDED;
+  }
 
   CHECK_ENOUGH(1, p, n);
   int code = (unsigned char)buf[p];
@@ -774,7 +778,8 @@ int fb_compact_unserialize_from_buffer(
       Array arr = Array::CreateVArray();
       while (p < n && buf[p] != (char)(kCodePrefix | FB_CS_STOP)) {
         Variant value;
-        int err = fb_compact_unserialize_from_buffer(value, buf, n, p);
+        int err =
+          fb_compact_unserialize_from_buffer(value, buf, n, p, depth + 1);
         if (err) {
           return err;
         }
@@ -799,7 +804,8 @@ int fb_compact_unserialize_from_buffer(
           ++p;
         } else {
           Variant value;
-          int err = fb_compact_unserialize_from_buffer(value, buf, n, p);
+          int err =
+            fb_compact_unserialize_from_buffer(value, buf, n, p, depth + 1);
           if (err) {
             return err;
           }
@@ -820,12 +826,12 @@ int fb_compact_unserialize_from_buffer(
       Array arr = Array::CreateDArray();
       while (p < n && buf[p] != (char)(kCodePrefix | FB_CS_STOP)) {
         Variant key;
-        int err = fb_compact_unserialize_from_buffer(key, buf, n, p);
+        int err = fb_compact_unserialize_from_buffer(key, buf, n, p, depth + 1);
         if (err) {
           return err;
         }
         Variant value;
-        err = fb_compact_unserialize_from_buffer(value, buf, n, p);
+        err = fb_compact_unserialize_from_buffer(value, buf, n, p, depth + 1);
         if (err) {
           return err;
         }
@@ -861,7 +867,7 @@ Variant fb_compact_unserialize(const char* str, int len,
 
   Variant ret;
   int p = 0;
-  int err = fb_compact_unserialize_from_buffer(ret, str, len, p);
+  int err = fb_compact_unserialize_from_buffer(ret, str, len, p, 0);
   if (err) {
     success = false;
     errcode = err;
@@ -1313,6 +1319,7 @@ struct FBExtension : Extension {
     HHVM_RC_INT_SAME(FB_UNSERIALIZE_UNEXPECTED_END);
     HHVM_RC_INT_SAME(FB_UNSERIALIZE_UNRECOGNIZED_OBJECT_TYPE);
     HHVM_RC_INT_SAME(FB_UNSERIALIZE_UNEXPECTED_ARRAY_KEY_TYPE);
+    HHVM_RC_INT_SAME(FB_UNSERIALIZE_MAX_DEPTH_EXCEEDED);
 
     HHVM_RC_INT(FB_SERIALIZE_HACK_ARRAYS, k_FB_SERIALIZE_HACK_ARRAYS);
     HHVM_RC_INT(FB_SERIALIZE_VARRAY_DARRAY, k_FB_SERIALIZE_VARRAY_DARRAY);

programming_language: C++
filename: unserialize-bad-set.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,9 @@
+<?hh
+// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+
+<<__EntryPoint>>
+function main() {
+  $ret = null;
+  var_dump(fb_unserialize("\x14\x02\x01\x14\x02\x01\x02\x01\x01\x01", inout $ret));
+  var_dump($ret);
+}

programming_language: PHP
filename: unserialize-bad-set.php.expect
change_type: ModificationType.ADD
diff: @@ -0,0 +1,2 @@
+bool(false)
+bool(false)

programming_language: SQL
filename: unserialize_max_depth.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,53 @@
+<?hh
+// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+
+function make_fb_serialize_struct($size) {
+  return str_repeat("\x0a\x02\x01", $size) . "\x02\x01" . str_repeat("\x01", $size);
+}
+
+function make_fb_serialize_vector($size) {
+  return str_repeat("\x12", $size) . str_repeat("\x01", $size);
+}
+
+function make_fb_serialize_list($size) {
+  return str_repeat("\x13\x02\x01", $size) . "\x02\x01" . str_repeat("\x01", $size);
+}
+
+function make_fb_cs_list_map($size) {
+  return str_repeat("\xfa\xfd", $size) . str_repeat("\xfc", $size);
+}
+
+function make_fb_cs_map($size) {
+  return str_repeat("\xfb\x01", $size) . "\x01" . str_repeat("\xfc", $size);
+}
+
+function make_fb_cs_vector($size) {
+  return str_repeat("\xfe", $size) . str_repeat("\xfc", $size);
+}
+
+function test($serialized) {
+  $ret = null;
+  var_dump(
+    fb_unserialize(
+      $serialized,
+      inout $ret,
+      FB_SERIALIZE_HACK_ARRAYS
+    )
+  );
+  var_dump($ret);
+}
+
+function tests($size) {
+  test(make_fb_serialize_struct($size));
+  test(make_fb_serialize_vector($size));
+  test(make_fb_serialize_list($size));
+  test(make_fb_cs_list_map($size));
+  test(make_fb_cs_map($size));
+  test(make_fb_cs_vector($size));
+}
+
+<<__EntryPoint>>
+function main() {
+  tests(10);
+  tests(1026);
+}

programming_language: PHP
filename: unserialize_max_depth.php.expect
change_type: ModificationType.ADD
diff: @@ -0,0 +1,179 @@
+dict(1) {
+  [1]=>
+  dict(1) {
+    [1]=>
+    dict(1) {
+      [1]=>
+      dict(1) {
+        [1]=>
+        dict(1) {
+          [1]=>
+          dict(1) {
+            [1]=>
+            dict(1) {
+              [1]=>
+              dict(1) {
+                [1]=>
+                dict(1) {
+                  [1]=>
+                  dict(1) {
+                    [1]=>
+                    int(1)
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+bool(true)
+vec(1) {
+  vec(1) {
+    vec(1) {
+      vec(1) {
+        vec(1) {
+          vec(1) {
+            vec(1) {
+              vec(1) {
+                vec(1) {
+                  vec(0) {
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+bool(true)
+vec(1) {
+  vec(1) {
+    vec(1) {
+      vec(1) {
+        vec(1) {
+          vec(1) {
+            vec(1) {
+              vec(1) {
+                vec(1) {
+                  vec(1) {
+                    int(1)
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+bool(true)
+array(1) {
+  [1]=>
+  array(1) {
+    [1]=>
+    array(1) {
+      [1]=>
+      array(1) {
+        [1]=>
+        array(1) {
+          [1]=>
+          array(1) {
+            [1]=>
+            array(1) {
+              [1]=>
+              array(1) {
+                [1]=>
+                array(1) {
+                  [1]=>
+                  array(0) {
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+bool(true)
+array(1) {
+  [1]=>
+  array(1) {
+    [1]=>
+    array(1) {
+      [1]=>
+      array(1) {
+        [1]=>
+        array(1) {
+          [1]=>
+          array(1) {
+            [1]=>
+            array(1) {
+              [1]=>
+              array(1) {
+                [1]=>
+                array(1) {
+                  [1]=>
+                  array(1) {
+                    [1]=>
+                    int(1)
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+bool(true)
+array(1) {
+  [0]=>
+  array(1) {
+    [0]=>
+    array(1) {
+      [0]=>
+      array(1) {
+        [0]=>
+        array(1) {
+          [0]=>
+          array(1) {
+            [0]=>
+            array(1) {
+              [0]=>
+              array(1) {
+                [0]=>
+                array(1) {
+                  [0]=>
+                  array(0) {
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+bool(true)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
\ No newline at end of file

programming_language: Swift