CVE-2019-3556 (published 2021-10-26T20:15Z)

HHVM supports the use of an "admin" server which accepts administrative requests over HTTP. One of those request handlers, dump-pcre-cache, can be used to output cached regular expressions from the current execution context into a file. The handler takes a parameter which specifies where on the filesystem to write this data. The parameter is not validated, allowing a malicious user to overwrite arbitrary files where the user running HHVM has write access. This issue affects HHVM versions prior to 4.56.2, all versions between 4.57.0 and 4.78.0, as well as 4.79.0, 4.80.0, 4.81.0, 4.82.0, and 4.83.0.

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

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/facebook/hhvm
commit_details: repo_url: https://github.com/facebook/hhvm
author: jjergus
author_date: 2020-11-11 13:34:05-08:00
msg: security fixes

https://hhvm.com/blog/2020/11/12/security-update.html
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: preg.cpp
change_type: ModificationType.MODIFY
diff: @@ -49,6 +49,7 @@
 #include "hphp/util/logger.h"
 #include "hphp/util/concurrent-scalable-cache.h"
 
+#include <folly/FileUtil.h>
 #include <folly/json.h>
 
 /* Only defined in pcre >= 8.32 */
@@ -217,7 +218,7 @@ public:
             TempKeyCache& keyCache);
   void insert(Accessor& accessor, const StringData* regex,
               TempKeyCache& keyCache, const pcre_cache_entry* ent);
-  void dump(const std::string& filename);
+  void dump(folly::File& file);
   size_t size() const;
 
 private:
@@ -512,12 +513,12 @@ void PCRECache::insert(
   }
 }
 
-void PCRECache::dump(const std::string& filename) {
-  std::ofstream out(filename.c_str());
+void PCRECache::dump(folly::File& file) {
   switch (m_kind) {
     case CacheKind::Static:
       for (auto& it : *m_staticCache) {
-        out << it.first->data() << "\n";
+        folly::writeFull(file.fd(), it.first->data(), it.first->size());
+        folly::writeFull(file.fd(), "\n", 1);
       }
       break;
     case CacheKind::Lru:
@@ -530,12 +531,12 @@ void PCRECache::dump(const std::string& filename) {
           m_scalableCache->snapshotKeys(keys);
         }
         for (auto& key: keys) {
-          out << key.c_str() << "\n";
+          folly::writeFull(file.fd(), key.data(), key.size());
+          folly::writeFull(file.fd(), "\n", 1);
         }
       }
       break;
   }
-  out.close();
 }
 
 size_t PCRECache::size() const {
@@ -572,8 +573,8 @@ void pcre_reinit() {
 void pcre_init() {
 }
 
-void pcre_dump_cache(const std::string& filename) {
-  s_pcreCache.dump(filename);
+void pcre_dump_cache(folly::File& file) {
+  s_pcreCache.dump(file);
 }
 
 static pcre_jit_stack* alloc_jit_stack(void* /*data*/) {

programming_language: C++
filename: preg.h
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,7 @@
 
 #include "hphp/runtime/base/type-string.h"
 
+#include <folly/File.h>
 #include <folly/Optional.h>
 
 #include <cstdint>
@@ -111,9 +112,9 @@ void pcre_reinit();
 void pcre_session_exit();
 
 /*
- * Dump the contents of the PCRE cache to filename.
+ * Dump the contents of the PCRE cache to the given file.
  */
-void pcre_dump_cache(const std::string& filename);
+void pcre_dump_cache(folly::File& file);
 
 ///////////////////////////////////////////////////////////////////////////////
 // PHP API

programming_language: C++
filename: runtime-option.cpp
change_type: ModificationType.MODIFY
diff: @@ -54,6 +54,7 @@
 #include "hphp/util/gzip.h"
 #include "hphp/util/hardware-counter.h"
 #include "hphp/util/hdf.h"
+#include "hphp/util/light-process.h"
 #include "hphp/util/log-file-flusher.h"
 #include "hphp/util/logger.h"
 #include "hphp/util/network.h"
@@ -754,6 +755,7 @@ bool RuntimeOption::AdminServerStatsNeedPassword = true;
 std::string RuntimeOption::AdminPassword;
 std::set<std::string> RuntimeOption::AdminPasswords;
 std::set<std::string> RuntimeOption::HashedAdminPasswords;
+std::string RuntimeOption::AdminDumpPath;
 
 std::string RuntimeOption::ProxyOriginRaw;
 int RuntimeOption::ProxyPercentageRaw = 0;
@@ -2398,6 +2400,8 @@ void RuntimeOption::Load(
                  "Server.LightProcessFilePrefix", "./lightprocess");
     Config::Bind(LightProcessCount, ini, config,
                  "Server.LightProcessCount", 0);
+    Config::Bind(LightProcess::g_strictUser, ini, config,
+                 "Server.LightProcessStrictUser", false);
     Config::Bind(ForceServerNameToHeader, ini, config,
                  "Server.ForceServerNameToHeader");
     Config::Bind(AllowDuplicateCookies, ini, config,
@@ -2513,6 +2517,8 @@ void RuntimeOption::Load(
     AdminPasswords = Config::GetSet(ini, config, "AdminServer.Passwords");
     HashedAdminPasswords =
       Config::GetSet(ini, config, "AdminServer.HashedPasswords");
+    Config::Bind(AdminDumpPath, ini, config,
+                 "AdminServer.DumpPath", "/tmp/hhvm_admin_dump");
   }
   {
     // Proxy

programming_language: C++
filename: runtime-option.h
change_type: ModificationType.MODIFY
diff: @@ -496,6 +496,8 @@ struct RuntimeOption {
   static std::set<std::string> AdminPasswords;
   static std::set<std::string> HashedAdminPasswords;
 
+  static std::string AdminDumpPath;
+
   /*
    * Options related to reverse proxying. ProxyOriginRaw and ProxyPercentageRaw
    * may be mutated by background threads and should only be read or written

programming_language: C++
filename: string-util.cpp
change_type: ModificationType.MODIFY
diff: @@ -435,7 +435,8 @@ String StringUtil::ROT13(const String& input) {
 }
 
 String StringUtil::Crypt(const String& input, const char *salt /* = "" */) {
-  if (salt && salt[0] == '\0') {
+  assertx(salt);
+  if (salt[0] == '\0') {
     raise_notice("crypt(): No salt parameter was specified."
       " You must use a randomly generated salt and a strong"
       " hash function to produce a secure hash.");

programming_language: C++
filename: ext_domdocument.h
change_type: ModificationType.MODIFY
diff: @@ -51,7 +51,9 @@ struct DOMNode {
   // for __clone
   DOMNode& operator=(const DOMNode& copy);
 
-  req::ptr<XMLDocumentData> doc() const { return m_node->doc(); }
+  req::ptr<XMLDocumentData> doc() const {
+    return m_node ? m_node->doc() : nullptr;
+  }
   XMLNode node() const { return m_node; }
   xmlNodePtr nodep() const {
     return m_node ? m_node->nodep() : nullptr;

programming_language: C++
filename: gd.cpp
change_type: ModificationType.MODIFY
diff: @@ -185,8 +185,8 @@ gdImagePtr gdImageCreateTrueColor (int sx, int sy)
 
   // Check for OOM before doing a potentially large allocation.
   auto allocsz = sizeof(gdImage)
-    + sy * (sizeof(int *) + sizeof(unsigned char *))
-    + sx * sy * (sizeof(int) + sizeof(unsigned char));
+    + (sizeof(int *) + sizeof(unsigned char *)) * sy
+    + (sizeof(int) + sizeof(unsigned char)) * sx * sy;
   if (UNLIKELY(precheckOOM(allocsz))) {
     // Don't throw here because GD might need to do its own cleanup.
     return NULL;

programming_language: C++
filename: ext_ldap.cpp
change_type: ModificationType.MODIFY
diff: @@ -2139,7 +2139,7 @@ String HHVM_FUNCTION(ldap_escape,
 
   char hex[] = "0123456789abcdef";
 
-  String result(3 * value.size(), ReserveString);
+  String result(3UL * value.size(), ReserveString);
   char *rdata = result.get()->mutableData(), *r = rdata;
 
   for (int i = 0; i < value.size(); i++) {

programming_language: C++
filename: ext_xmlreader.cpp
change_type: ModificationType.MODIFY
diff: @@ -598,11 +598,11 @@ Variant HHVM_METHOD(XMLReader, expand,
   if (!basenode.isNull()) {
     auto dombasenode = Native::data<DOMNode>(basenode.toObject());
     doc = dombasenode->doc();
-    docp = doc->docp();
-    if (docp == nullptr) {
+    if (doc == nullptr || doc->docp() == nullptr) {
       raise_warning("Invalid State Error");
       return false;
     }
+    docp = doc->docp();
   }
 
   if (data->m_ptr) {

programming_language: C++
filename: admin-request-handler.cpp
change_type: ModificationType.MODIFY
diff: @@ -76,6 +76,9 @@
 #endif
 
 #include <folly/Conv.h>
+#include <folly/File.h>
+#include <folly/FileUtil.h>
+#include <folly/Optional.h>
 #include <folly/Random.h>
 #include <folly/portability/Unistd.h>
 
@@ -254,6 +257,34 @@ void AdminRequestHandler::teardownRequest(Transport* transport) noexcept {
   WarnIfNotOK(transport);
 }
 
+namespace {
+
+// When this struct is destroyed, it will close the file.
+struct DumpFile {
+  std::string path;
+  folly::File file;
+};
+
+folly::Optional<DumpFile> dump_file(const char* name) {
+  auto const path = folly::sformat("{}/{}", RO::AdminDumpPath, name);
+
+  // mkdir -p the directory prefix of `path`
+  if (FileUtil::mkdir(path) != 0) return folly::none;
+
+  // If remove fails because of a permissions issue, then we won't be
+  // able to open the file for exclusive write below.
+  remove(path.c_str());
+
+  // Create the file, failing if it already exists. Doing so ensures
+  // that we have write access to the file and that no other user does.
+  auto const fd = open(path.c_str(), O_CREAT|O_EXCL|O_RDWR, 0666);
+  if (fd < 0) return folly::none;
+
+  return DumpFile{path, folly::File(fd, /*owns=*/true)};
+}
+
+}
+
 void AdminRequestHandler::handleRequest(Transport *transport) {
   transport->addHeader("Content-Type", "text/plain");
   std::string cmd = transport->getCommand();
@@ -714,10 +745,12 @@ void AdminRequestHandler::handleRequest(Transport *transport) {
       break;
     }
     if (strncmp(cmd.c_str(), "dump-static-strings", 19) == 0) {
-      auto filename = transport->getParam("file");
-      if (filename == "") filename = "/tmp/static_strings";
-      handleDumpStaticStringsRequest(cmd, filename);
-      transport->sendString("OK\n");
+      if (auto file = dump_file("static_strings")) {
+        handleDumpStaticStringsRequest(file->file);
+        transport->sendString(folly::sformat("dumped to {}\n", file->path));
+      } else {
+        transport->sendString("Unable to mkdir or file already exists.\n");
+      }
       break;
     }
     if (strncmp(cmd.c_str(), "random-static-strings", 21) == 0) {
@@ -746,10 +779,12 @@ void AdminRequestHandler::handleRequest(Transport *transport) {
     }
 
     if (cmd == "dump-pcre-cache") {
-      auto filename = transport->getParam("file");
-      if (filename == "") filename = "/tmp/pcre_cache";
-      pcre_dump_cache(filename);
-      transport->sendString("OK\n");
+      if (auto file = dump_file("pcre_cache")) {
+        pcre_dump_cache(file->file);
+        transport->sendString(folly::sformat("dumped to {}\n", file->path));
+      } else {
+        transport->sendString("Unable to mkdir or file already exists.\n");
+      }
       break;
     }
 
@@ -1368,13 +1403,11 @@ std::string formatStaticString(StringData* str) {
       "----\n{} bytes\n{}\n", str->size(), str->toCppString());
 }
 
-bool AdminRequestHandler::handleDumpStaticStringsRequest(
-  const std::string& /*cmd*/, const std::string& filename) {
+bool AdminRequestHandler::handleDumpStaticStringsRequest(folly::File& file) {
   auto const& list = lookupDefinedStaticStrings();
-  std::ofstream out(filename.c_str());
-  SCOPE_EXIT { out.close(); };
   for (auto item : list) {
-    out << formatStaticString(item);
+    auto const line = formatStaticString(item);
+    folly::writeFull(file.fd(), line.data(), line.size());
     if (RuntimeOption::EvalPerfDataMap) {
       auto const len = std::min<size_t>(item->size(), 255);
       std::string str(item->data(), len);

programming_language: C++
filename: admin-request-handler.h
change_type: ModificationType.MODIFY
diff: @@ -19,6 +19,8 @@
 #include "hphp/runtime/server/access-log.h"
 #include "hphp/runtime/server/server.h"
 
+#include <folly/File.h>
+
 namespace HPHP {
 ///////////////////////////////////////////////////////////////////////////////
 
@@ -72,8 +74,7 @@ private:
                                    Transport *transport);
   bool handleStaticStringsRequest(const std::string &cmd,
                                   Transport *transport);
-  bool handleDumpStaticStringsRequest(const std::string &cmd,
-                                      const std::string &filename);
+  bool handleDumpStaticStringsRequest(folly::File& file);
   bool handleRandomStaticStringsRequest(const std::string &cmd,
                                         Transport *transport);
   bool handleVMRequest      (const std::string &cmd, Transport *transport);

programming_language: C++
filename: t78806688.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,9 @@
+<?hh
+
+<<__EntryPoint>>
+function main(): void {
+  $multiplier = 1431655769;
+  $s = str_repeat("a", $multiplier);
+  ldap_escape($s);
+  echo "FAIL!\n";
+}

programming_language: None
filename: t78806688.php.expectf
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+Fatal error: String length exceeded: 4294967307 > %d in %s/t78806688.php on line 7

programming_language: Batchfile
filename: t76103217.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,11 @@
+<?hh
+
+<<__EntryPoint>>
+function main(): void {
+  $key = "\x26\xbd\xbd\xbd\xff\x60\xbf\xff\xff\x60";
+  $salt1 = "\x24\x32\x78\x24\x31\x30\x24\x24\x35\x24\xad\x20\x20\x26\xff\x60\xbf\xff\xff\x60\x24\x31\x78\xa8\xa8\xa0\x01\x01\x01\x01\x01\x01";
+  $salt2 = "\x24\x32\x78\x24\x31\x30\x24\x24\x35";
+
+  var_dump(base64_encode(crypt($key, $salt1)));
+  var_dump(base64_encode(crypt($key, $salt2)));
+}

programming_language: Perl
filename: t76103217.php.expect
change_type: ModificationType.ADD
diff: @@ -0,0 +1,2 @@
+string(4) "KjA="
+string(12) "JDJ4JDEwJCQ1"

programming_language: Batchfile
filename: xmlreader_empty_expand.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,9 @@
+<?hh
+
+<<__EntryPoint>>
+function main(): void {
+    $a = new XMLReader();
+    $b = new DOMNode();
+    $a->expand($b);
+    $a->expand($a);
+}

programming_language: PHP
filename: xmlreader_empty_expand.php.expectf
change_type: ModificationType.ADD
diff: @@ -0,0 +1,3 @@
+Warning: Invalid State Error in %s on line %d
+
+Warning: Invalid State Error in %s on line %d

programming_language: Batchfile
filename: light-process.cpp
change_type: ModificationType.MODIFY
diff: @@ -46,6 +46,7 @@
 #include "hphp/util/hugetlb.h"
 #include "hphp/util/logger.h"
 #include "hphp/util/process.h"
+#include "hphp/util/struct-log.h"
 #include "hphp/util/timer.h"
 #include "hphp/util/user-info.h"
 
@@ -54,6 +55,8 @@ namespace HPHP {
 ///////////////////////////////////////////////////////////////////////////////
 // helper functions
 
+bool LightProcess::g_strictUser = false;
+
 namespace {
 
 __thread LightProcess* tl_proc;
@@ -436,23 +439,63 @@ void do_change_user(int afdt_fd) {
   lwp_read(afdt_fd, uname);
   if (!uname.length()) return;
 
+  StructuredLogEntry* log = nullptr;
+  int err = 0;
+  SCOPE_EXIT {
+    if (log) {
+      log->setInt("errno", err);
+      log->setStr("new_user", uname);
+      StructuredLog::log("hhvm_lightprocess_error", *log);
+      delete log;
+    }
+  };
   auto buf = PasswdBuffer{};
   struct passwd *pw;
   if (getpwnam_r(uname.c_str(), &buf.ent, buf.data.get(), buf.size, &pw)) {
-    // TODO(alexeyt) should we log something and/or fail to start?
+    err = errno;
+    log = new StructuredLogEntry();
+    log->setStr("function", "getpwnam_r");
+    if (LightProcess::g_strictUser) {
+      throw std::runtime_error{"getpwnam_r(): " + folly::errnoStr(err)};
+    }
     return;
   }
   if (!pw) {
-    // TODO(alexeyt) should we log something and/or fail to start?
+    log = new StructuredLogEntry();
+    log->setStr("function", "getpwnam_r");
+    if (LightProcess::g_strictUser) {
+      throw std::runtime_error{"getpwnam_r(): not found"};
+    }
     return;
   }
-
   if (pw->pw_gid) {
-    initgroups(pw->pw_name, pw->pw_gid);
-    setgid(pw->pw_gid);
+    if (initgroups(pw->pw_name, pw->pw_gid)) {
+      err = errno;
+      log = new StructuredLogEntry();
+      log->setStr("function", "initgroups");
+    }
+    if (setgid(pw->pw_gid)) {
+      if (!log) {
+        err = errno;
+        log = new StructuredLogEntry();
+        log->setStr("function", "setgid");
+        if (LightProcess::g_strictUser) {
+          throw std::runtime_error{"setgid():" + folly::errnoStr(err)};
+        }
+      }
+    }
   }
   if (pw->pw_uid) {
-    setuid(pw->pw_uid);
+    if (setuid(pw->pw_uid)) {
+      if (!log) {
+        err = errno;
+        log = new StructuredLogEntry();
+        log->setStr("function", "setuid");
+        if (LightProcess::g_strictUser) {
+          throw std::runtime_error{"setuid():" + folly::errnoStr(err)};
+        }
+      }
+    }
   }
 }
 

programming_language: C++
filename: light-process.h
change_type: ModificationType.MODIFY
diff: @@ -129,6 +129,10 @@ private:
   Mutex m_procMutex;
   int m_afdt_fd;
   std::map<FILE*, pid_t> m_popenMap;
+
+ public:
+  // whether to abort when user change fails.
+  static bool g_strictUser;
 };
 
 ///////////////////////////////////////////////////////////////////////////////

programming_language: C++
filename: crypt-blowfish.cpp
change_type: ModificationType.MODIFY
diff: @@ -696,7 +696,10 @@ static char *BF_crypt(const char *key, const char *setting,
   }
 
   count = (BF_word)1 << ((setting[4] - '0') * 10 + (setting[5] - '0'));
-  if (count < min || BF_decode(data.binary.salt, &setting[7], 16)) {
+  if (count < min ||
+      BF_decode(data.binary.salt, &setting[7], 16) ||
+      ((unsigned int)(setting[7 + 22 - 1] - 0x20) >= 0x60)
+     ) {
     __set_errno(EINVAL);
     return NULL;
   }
@@ -914,4 +917,3 @@ char *_crypt_gensalt_blowfish_rn(const char *prefix, unsigned long count,
   return output;
 }
 #endif
-

programming_language: C
filename: zend-string.cpp
change_type: ModificationType.MODIFY
diff: @@ -396,8 +396,8 @@ static void ito64(char *s, long v, int n) {
 }
 
 char *string_crypt(const char *key, const char *salt) {
-  assert(key);
-  assert(salt);
+  assertx(key);
+  assertx(salt);
 
   char random_salt[12];
   if (!*salt) {
@@ -407,7 +407,8 @@ char *string_crypt(const char *key, const char *salt) {
     return string_crypt(key, random_salt);
   }
 
-  if ((strlen(salt) > sizeof("$2X$00$")) &&
+  auto const saltLen = strlen(salt);
+  if ((saltLen > sizeof("$2X$00$")) &&
     (salt[0] == '$') &&
     (salt[1] == '2') &&
     (salt[2] >= 'a') && (salt[2] <= 'z') &&
@@ -417,7 +418,16 @@ char *string_crypt(const char *key, const char *salt) {
     (salt[6] == '$')) {
     // Bundled blowfish crypt()
     char output[61];
-    if (php_crypt_blowfish_rn(key, salt, output, sizeof(output))) {
+
+    static constexpr size_t maxSaltLength = 123;
+    char paddedSalt[maxSaltLength + 1];
+    paddedSalt[0] = paddedSalt[maxSaltLength] = '\0';
+
+    memset(&paddedSalt[1], '$', maxSaltLength - 1);
+    memcpy(paddedSalt, salt, std::min(maxSaltLength, saltLen));
+    paddedSalt[saltLen] = '\0';
+
+    if (php_crypt_blowfish_rn(key, paddedSalt, output, sizeof(output))) {
       return strdup(output);
     }
 

programming_language: C