Bug 1174555 - Stop using PR_sscanf() in nsSiteSecurityService.cpp. r=keeler draft
authorCykesiopka <cykesiopka.bmo@gmail.com>
Sun, 21 May 2017 10:43:18 +0800
changeset 582088 e77e8b1ba70bef6f0ff794b7d066bbbdebe8f58e
parent 582001 5b74bbf20e803e299790d266fc6ebf5d53b7a1b7
child 582089 ba47e0cfe9317703895df02277568e59cc56591c
push id59968
push usercykesiopka.bmo@gmail.com
push dateSun, 21 May 2017 05:35:57 +0000
reviewerskeeler
bugs1174555
milestone55.0a1
Bug 1174555 - Stop using PR_sscanf() in nsSiteSecurityService.cpp. r=keeler While the uses of PR_sscanf() in PSM are safe, the function in general is vulnerable to format string attacks, and so should be avoided. This change removes the only uses of the function in PSM and moves to the more obviously safe mozilla::Tokenizer. MozReview-Commit-ID: J4BP6JTE1zI
security/manager/ssl/nsSiteSecurityService.cpp
--- a/security/manager/ssl/nsSiteSecurityService.cpp
+++ b/security/manager/ssl/nsSiteSecurityService.cpp
@@ -4,40 +4,40 @@
 
 #include "nsSiteSecurityService.h"
 
 #include "CertVerifier.h"
 #include "PublicKeyPinningService.h"
 #include "ScopedNSSTypes.h"
 #include "SharedCertVerifier.h"
 #include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
 #include "mozilla/Base64.h"
-#include "mozilla/dom/PContent.h"
-#include "mozilla/dom/ToJSValue.h"
 #include "mozilla/LinkedList.h"
 #include "mozilla/Logging.h"
 #include "mozilla/Preferences.h"
+#include "mozilla/Tokenizer.h"
+#include "mozilla/dom/PContent.h"
+#include "mozilla/dom/ToJSValue.h"
 #include "nsArrayEnumerator.h"
 #include "nsCOMArray.h"
 #include "nsISSLStatus.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsISocketProvider.h"
 #include "nsIURI.h"
 #include "nsIX509Cert.h"
 #include "nsNSSComponent.h"
 #include "nsNetUtil.h"
 #include "nsPromiseFlatString.h"
 #include "nsReadableUtils.h"
 #include "nsSecurityHeaderParser.h"
 #include "nsThreadUtils.h"
+#include "nsVariant.h"
 #include "nsXULAppAPI.h"
-#include "nsVariant.h"
-#include "plstr.h"
 #include "prnetdb.h"
-#include "prprf.h"
 
 // A note about the preload list:
 // When a site specifically disables HSTS by sending a header with
 // 'max-age: 0', we keep a "knockout" value that means "we have no information
 // regarding the HSTS state of this host" (any ancestor of "this host" can still
 // influence its HSTS status via include subdomains, however).
 // This prevents the preload list from overriding the site's current
 // desired HSTS status.
@@ -52,40 +52,153 @@ static LazyLogModule gSSSLog("nsSSServic
 
 const char kHSTSKeySuffix[] = ":HSTS";
 const char kHPKPKeySuffix[] = ":HPKP";
 
 ////////////////////////////////////////////////////////////////////////////////
 
 NS_IMPL_ISUPPORTS(SiteHSTSState, nsISiteSecurityState, nsISiteHSTSState)
 
+namespace {
+
+static bool
+stringIsBase64EncodingOf256bitValue(const nsCString& encodedString) {
+  nsAutoCString binaryValue;
+  nsresult rv = Base64Decode(encodedString, binaryValue);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  return binaryValue.Length() == SHA256_LENGTH;
+}
+
+class SSSTokenizer final : public Tokenizer
+{
+public:
+  explicit SSSTokenizer(const nsACString& source)
+    : Tokenizer(source)
+  {
+  }
+
+  MOZ_MUST_USE bool
+  ReadBool(/*out*/ bool& value)
+  {
+    uint8_t rawValue;
+    if (!ReadInteger(&rawValue)) {
+      return false;
+    }
+
+    if (rawValue != 0 && rawValue != 1) {
+      return false;
+    }
+
+    value = (rawValue == 1);
+    return true;
+  }
+
+  MOZ_MUST_USE bool
+  ReadState(/*out*/ SecurityPropertyState& state)
+  {
+    uint32_t rawValue;
+    if (!ReadInteger(&rawValue)) {
+      return false;
+    }
+
+    state = static_cast<SecurityPropertyState>(rawValue);
+    switch (state) {
+      case SecurityPropertyKnockout:
+      case SecurityPropertyNegative:
+      case SecurityPropertySet:
+      case SecurityPropertyUnset:
+        break;
+      default:
+        return false;
+    }
+
+    return true;
+  }
+
+  // Note: Ideally, this method would be able to read SHA256 strings without
+  // reading all the way to EOF. Unfortunately, if a token starts with digits
+  // mozilla::Tokenizer will by default not consider the digits part of the
+  // string. This can be worked around by making mozilla::Tokenizer consider
+  // digit characters as "word" characters as well, but this can't be changed at
+  // run time, meaning parsing digits as a number will fail.
+  MOZ_MUST_USE bool
+  ReadUntilEOFAsSHA256Keys(/*out*/ nsTArray<nsCString>& keys)
+  {
+    nsAutoCString mergedKeys;
+    if (!ReadUntil(Token::EndOfFile(), mergedKeys, EXCLUDE_LAST)) {
+      return false;
+    }
+
+    // This check makes sure the Substring() calls below are always valid.
+    static const uint32_t SHA256Base64Len = 44;
+    if (mergedKeys.Length() % SHA256Base64Len != 0) {
+      return false;
+    }
+
+    for (uint32_t i = 0; i < mergedKeys.Length(); i += SHA256Base64Len) {
+      nsAutoCString key(Substring(mergedKeys, i, SHA256Base64Len));
+      if (!stringIsBase64EncodingOf256bitValue(key)) {
+        return false;
+      }
+      keys.AppendElement(key);
+    }
+
+    return !keys.IsEmpty();
+  }
+};
+
+// Parses a state string like "1500918564034,1,1" into its constituent parts.
+bool
+ParseHSTSState(const nsCString& stateString,
+       /*out*/ PRTime& expireTime,
+       /*out*/ SecurityPropertyState& state,
+       /*out*/ bool& includeSubdomains)
+{
+  SSSTokenizer tokenizer(stateString);
+
+  if (!tokenizer.ReadInteger(&expireTime)) {
+    return false;
+  }
+
+  if (!tokenizer.CheckChar(',')) {
+    return false;
+  }
+
+  if (!tokenizer.ReadState(state)) {
+    return false;
+  }
+
+  if (!tokenizer.CheckChar(',')) {
+    return false;
+  }
+
+  if (!tokenizer.ReadBool(includeSubdomains)) {
+    return false;
+  }
+
+  return tokenizer.CheckEOF();
+}
+
+} // namespace
+
 SiteHSTSState::SiteHSTSState(const nsCString& aHost,
                              const OriginAttributes& aOriginAttributes,
                              const nsCString& aStateString)
   : mHostname(aHost)
   , mOriginAttributes(aOriginAttributes)
   , mHSTSExpireTime(0)
   , mHSTSState(SecurityPropertyUnset)
   , mHSTSIncludeSubdomains(false)
 {
-  uint32_t hstsState = 0;
-  uint32_t hstsIncludeSubdomains = 0; // PR_sscanf doesn't handle bools.
-  int32_t matches = PR_sscanf(aStateString.get(), "%lld,%lu,%lu",
-                              &mHSTSExpireTime, &hstsState,
-                              &hstsIncludeSubdomains);
-  bool valid = (matches == 3 &&
-                (hstsIncludeSubdomains == 0 || hstsIncludeSubdomains == 1) &&
-                ((SecurityPropertyState)hstsState == SecurityPropertyUnset ||
-                 (SecurityPropertyState)hstsState == SecurityPropertySet ||
-                 (SecurityPropertyState)hstsState == SecurityPropertyKnockout ||
-                 (SecurityPropertyState)hstsState == SecurityPropertyNegative));
-  if (valid) {
-    mHSTSState = (SecurityPropertyState)hstsState;
-    mHSTSIncludeSubdomains = (hstsIncludeSubdomains == 1);
-  } else {
+  bool valid = ParseHSTSState(aStateString, mHSTSExpireTime, mHSTSState,
+                              mHSTSIncludeSubdomains);
+  if (!valid) {
     SSSLOG(("%s is not a valid SiteHSTSState", aStateString.get()));
     mHSTSExpireTime = 0;
     mHSTSState = SecurityPropertyUnset;
     mHSTSIncludeSubdomains = false;
   }
 }
 
 SiteHSTSState::SiteHSTSState(const nsCString& aHost,
@@ -153,91 +266,95 @@ SiteHSTSState::GetOriginAttributes(JSCon
   }
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 
 NS_IMPL_ISUPPORTS(SiteHPKPState, nsISiteSecurityState, nsISiteHPKPState)
 
-static bool
-stringIsBase64EncodingOf256bitValue(nsCString& encodedString) {
-  nsAutoCString binaryValue;
-  nsresult rv = mozilla::Base64Decode(encodedString, binaryValue);
-  if (NS_FAILED(rv)) {
+namespace {
+
+// Parses a state string like
+// "1494603034103,1,1,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" into its
+// constituent parts.
+bool
+ParseHPKPState(const nsCString& stateString,
+       /*out*/ PRTime& expireTime,
+       /*out*/ SecurityPropertyState& state,
+       /*out*/ bool& includeSubdomains,
+       /*out*/ nsTArray<nsCString>& sha256keys)
+{
+  SSSTokenizer tokenizer(stateString);
+
+  if (!tokenizer.ReadInteger(&expireTime)) {
+    return false;
+  }
+
+  if (!tokenizer.CheckChar(',')) {
+    return false;
+  }
+
+  if (!tokenizer.ReadState(state)) {
     return false;
   }
-  if (binaryValue.Length() != SHA256_LENGTH) {
+
+  // SecurityPropertyNegative isn't a valid state for HPKP.
+  switch (state) {
+    case SecurityPropertyKnockout:
+    case SecurityPropertySet:
+    case SecurityPropertyUnset:
+      break;
+    case SecurityPropertyNegative:
+    default:
+      return false;
+  }
+
+  if (!tokenizer.CheckChar(',')) {
     return false;
   }
-  return true;
+
+  if (!tokenizer.ReadBool(includeSubdomains)) {
+    return false;
+  }
+
+  if (!tokenizer.CheckChar(',')) {
+    return false;
+  }
+
+  if (state == SecurityPropertySet) {
+    // This reads to the end of input, so there's no need to explicitly check
+    // for EOF.
+    return tokenizer.ReadUntilEOFAsSHA256Keys(sha256keys);
+  }
+
+  return tokenizer.CheckEOF();
 }
 
+} // namespace
+
 SiteHPKPState::SiteHPKPState()
   : mExpireTime(0)
   , mState(SecurityPropertyUnset)
   , mIncludeSubdomains(false)
 {
 }
 
 SiteHPKPState::SiteHPKPState(const nsCString& aHost,
                              const OriginAttributes& aOriginAttributes,
                              const nsCString& aStateString)
   : mHostname(aHost)
   , mOriginAttributes(aOriginAttributes)
   , mExpireTime(0)
   , mState(SecurityPropertyUnset)
   , mIncludeSubdomains(false)
 {
-  uint32_t hpkpState = 0;
-  uint32_t hpkpIncludeSubdomains = 0; // PR_sscanf doesn't handle bools.
-  const uint32_t MaxMergedHPKPPinSize = 1024;
-  char mergedHPKPins[MaxMergedHPKPPinSize];
-  memset(mergedHPKPins, 0, MaxMergedHPKPPinSize);
-
-  if (aStateString.Length() >= MaxMergedHPKPPinSize) {
-    SSSLOG(("SSS: Cannot parse PKPState string, too large\n"));
-    return;
-  }
-
-  int32_t matches = PR_sscanf(aStateString.get(), "%lld,%lu,%lu,%s",
-                              &mExpireTime, &hpkpState,
-                              &hpkpIncludeSubdomains, mergedHPKPins);
-  bool valid = (matches == 4 &&
-                (hpkpIncludeSubdomains == 0 || hpkpIncludeSubdomains == 1) &&
-                ((SecurityPropertyState)hpkpState == SecurityPropertyUnset ||
-                 (SecurityPropertyState)hpkpState == SecurityPropertySet ||
-                 (SecurityPropertyState)hpkpState == SecurityPropertyKnockout));
-
-  SSSLOG(("SSS: loading SiteHPKPState matches=%d\n", matches));
-  const uint32_t SHA256Base64Len = 44;
-
-  if (valid && (SecurityPropertyState)hpkpState == SecurityPropertySet) {
-    // try to expand the merged PKPins
-    const char* cur = mergedHPKPins;
-    nsAutoCString pin;
-    uint32_t collectedLen = 0;
-    mergedHPKPins[MaxMergedHPKPPinSize - 1] = 0;
-    size_t totalLen = strlen(mergedHPKPins);
-    while (collectedLen + SHA256Base64Len <= totalLen) {
-      pin.Assign(cur, SHA256Base64Len);
-      if (stringIsBase64EncodingOf256bitValue(pin)) {
-        mSHA256keys.AppendElement(pin);
-      }
-      cur += SHA256Base64Len;
-      collectedLen += SHA256Base64Len;
-    }
-    if (mSHA256keys.IsEmpty()) {
-      valid = false;
-    }
-  }
-  if (valid) {
-    mState = (SecurityPropertyState)hpkpState;
-    mIncludeSubdomains = (hpkpIncludeSubdomains == 1);
-  } else {
+  bool valid = ParseHPKPState(aStateString, mExpireTime, mState,
+                              mIncludeSubdomains, mSHA256keys);
+  if (!valid) {
     SSSLOG(("%s is not a valid SiteHPKPState", aStateString.get()));
     mExpireTime = 0;
     mState = SecurityPropertyUnset;
     mIncludeSubdomains = false;
     if (!mSHA256keys.IsEmpty()) {
       mSHA256keys.Clear();
     }
   }
@@ -822,27 +939,24 @@ ParseSSSHeaders(uint32_t aType,
       if (foundMaxAge) {
         SSSLOG(("SSS: found two max-age directives"));
         return nsISiteSecurityService::ERROR_MULTIPLE_MAX_AGES;
       }
 
       SSSLOG(("SSS: found max-age directive"));
       foundMaxAge = true;
 
-      size_t len = directive->mValue.Length();
-      for (size_t i = 0; i < len; i++) {
-        char chr = directive->mValue.CharAt(i);
-        if (chr < '0' || chr > '9') {
-          SSSLOG(("SSS: invalid value for max-age directive"));
-          return nsISiteSecurityService::ERROR_INVALID_MAX_AGE;
-        }
+      Tokenizer tokenizer(directive->mValue);
+      if (!tokenizer.ReadInteger(&maxAge)) {
+        SSSLOG(("SSS: could not parse delta-seconds"));
+        return nsISiteSecurityService::ERROR_INVALID_MAX_AGE;
       }
 
-      if (PR_sscanf(directive->mValue.get(), "%llu", &maxAge) != 1) {
-        SSSLOG(("SSS: could not parse delta-seconds"));
+      if (!tokenizer.CheckEOF()) {
+        SSSLOG(("SSS: invalid value for max-age directive"));
         return nsISiteSecurityService::ERROR_INVALID_MAX_AGE;
       }
 
       SSSLOG(("SSS: parsed delta-seconds: %" PRIu64, maxAge));
     } else if (directive->mName.Length() == include_subd_var.Length() &&
                directive->mName.EqualsIgnoreCase(include_subd_var.get(),
                                                  include_subd_var.Length())) {
       if (foundIncludeSubdomains) {