SRICheck.cpp (18114B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "SRICheck.h" 8 9 #include "mozilla/Base64.h" 10 #include "mozilla/LoadTainting.h" 11 #include "mozilla/Logging.h" 12 #include "mozilla/dom/SRILogHelper.h" 13 #include "mozilla/dom/SRIMetadata.h" 14 #include "nsContentUtils.h" 15 #include "nsIChannel.h" 16 #include "nsIConsoleReportCollector.h" 17 #include "nsIHttpChannel.h" 18 #include "nsIScriptError.h" 19 #include "nsIURI.h" 20 #include "nsNetUtil.h" 21 #include "nsWhitespaceTokenizer.h" 22 23 #define SRIVERBOSE(args) \ 24 MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Verbose, args) 25 #define SRILOG(args) \ 26 MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug, args) 27 #define SRIERROR(args) \ 28 MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Error, args) 29 30 namespace mozilla::dom { 31 32 static void GetChannelRequestURI(nsIChannel* aChannel, nsACString& aSourceUri) { 33 nsCOMPtr<nsIURI> originalURI; 34 aChannel->GetOriginalURI(getter_AddRefs(originalURI)); 35 if (originalURI) { 36 originalURI->GetAsciiSpec(aSourceUri); 37 } 38 } 39 40 static void GetReferrerSpec(nsIChannel* aChannel, nsACString& aReferrer) { 41 nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(aChannel); 42 if (!httpChan) { 43 return; 44 } 45 nsCOMPtr<nsIReferrerInfo> referrer = httpChan->GetReferrerInfo(); 46 if (!referrer) { 47 return; 48 } 49 nsCOMPtr<nsIURI> original = referrer->GetOriginalReferrer(); 50 if (!original) { 51 return; 52 } 53 original->GetSpec(aReferrer); 54 } 55 56 /** 57 * Returns whether or not the sub-resource about to be loaded is eligible 58 * for integrity checks. If it's not, the checks will be skipped and the 59 * sub-resource will be loaded. 60 */ 61 static nsresult IsEligible(nsIChannel* aChannel, 62 mozilla::LoadTainting aTainting, 63 nsIConsoleReportCollector* aReporter) { 64 NS_ENSURE_ARG_POINTER(aReporter); 65 66 if (!aChannel) { 67 SRILOG(("SRICheck::IsEligible, null channel")); 68 return NS_ERROR_SRI_NOT_ELIGIBLE; 69 } 70 71 // Was the sub-resource loaded via CORS? 72 if (aTainting == LoadTainting::CORS) { 73 SRILOG(("SRICheck::IsEligible, CORS mode")); 74 return NS_OK; 75 } 76 77 // Is the sub-resource same-origin? 78 if (aTainting == LoadTainting::Basic) { 79 SRILOG(("SRICheck::IsEligible, same-origin")); 80 return NS_OK; 81 } 82 SRILOG(("SRICheck::IsEligible, NOT same-origin")); 83 84 nsAutoCString requestSpec; 85 GetChannelRequestURI(aChannel, requestSpec); 86 nsAutoCString referrer; 87 GetReferrerSpec(aChannel, referrer); 88 aReporter->AddConsoleReport( 89 nsIScriptError::errorFlag, "Sub-resource Integrity"_ns, 90 nsContentUtils::eSECURITY_PROPERTIES, referrer, 0, 0, 91 "IneligibleResource"_ns, {NS_ConvertUTF8toUTF16(requestSpec)}); 92 return NS_ERROR_SRI_NOT_ELIGIBLE; 93 } 94 95 /* static */ 96 nsresult SRICheck::IntegrityMetadata(const nsAString& aMetadataList, 97 const nsACString& aSourceFileURI, 98 nsIConsoleReportCollector* aReporter, 99 SRIMetadata* outMetadata) { 100 NS_ENSURE_ARG_POINTER(outMetadata); 101 MOZ_ASSERT(outMetadata->IsEmpty()); // caller must pass empty metadata 102 103 NS_ConvertUTF16toUTF8 metadataList(aMetadataList); 104 SRILOG(("SRICheck::IntegrityMetadata, metadataList=%s", metadataList.get())); 105 106 // the integrity attribute is a list of whitespace-separated hashes 107 // and options so we need to look at them one by one and pick the 108 // strongest (valid) one 109 nsCWhitespaceTokenizer tokenizer(metadataList); 110 nsAutoCString token; 111 while (tokenizer.hasMoreTokens()) { 112 token = tokenizer.nextToken(); 113 114 SRIMetadata metadata(token); 115 if (aReporter && metadata.IsMalformed()) { 116 aReporter->AddConsoleReport( 117 nsIScriptError::warningFlag, "Sub-resource Integrity"_ns, 118 nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0, 119 "MalformedIntegrityHash"_ns, {NS_ConvertUTF8toUTF16(token)}); 120 } else if (aReporter && !metadata.IsAlgorithmSupported()) { 121 nsAutoCString alg; 122 metadata.GetAlgorithm(&alg); 123 aReporter->AddConsoleReport( 124 nsIScriptError::warningFlag, "Sub-resource Integrity"_ns, 125 nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0, 126 "UnsupportedHashAlg"_ns, {NS_ConvertUTF8toUTF16(alg)}); 127 } 128 129 nsAutoCString alg1, alg2; 130 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) { 131 outMetadata->GetAlgorithm(&alg1); 132 metadata.GetAlgorithm(&alg2); 133 } 134 if (*outMetadata == metadata) { 135 SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is the same as '%s'", 136 alg1.get(), alg2.get())); 137 *outMetadata += metadata; // add new hash to strongest metadata 138 } else if (*outMetadata < metadata) { 139 SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is weaker than '%s'", 140 alg1.get(), alg2.get())); 141 *outMetadata = metadata; // replace strongest metadata with current 142 } 143 } 144 145 outMetadata->mIntegrityString = aMetadataList; 146 147 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) { 148 if (outMetadata->IsValid()) { 149 nsAutoCString alg; 150 outMetadata->GetAlgorithm(&alg); 151 SRILOG(("SRICheck::IntegrityMetadata, using a '%s' hash", alg.get())); 152 } else if (outMetadata->IsEmpty()) { 153 SRILOG(("SRICheck::IntegrityMetadata, no metadata")); 154 } else { 155 SRILOG(("SRICheck::IntegrityMetadata, no valid metadata found")); 156 } 157 } 158 return NS_OK; 159 } 160 161 ////////////////////////////////////////////////////////////// 162 // 163 ////////////////////////////////////////////////////////////// 164 SRICheckDataVerifier::SRICheckDataVerifier(const SRIMetadata& aMetadata, 165 nsIChannel* aChannel, 166 nsIConsoleReportCollector* aReporter) 167 : mCryptoHash(nullptr), 168 mBytesHashed(0), 169 mHashLength(0), 170 mHashType('\0'), 171 mInvalidMetadata(false), 172 mComplete(false) { 173 MOZ_ASSERT(!aMetadata.IsEmpty()); // should be checked by caller 174 MOZ_ASSERT(aReporter); 175 176 if (!aMetadata.IsValid()) { 177 nsAutoCString referrer; 178 GetReferrerSpec(aChannel, referrer); 179 aReporter->AddConsoleReport(nsIScriptError::warningFlag, 180 "Sub-resource Integrity"_ns, 181 nsContentUtils::eSECURITY_PROPERTIES, referrer, 182 0, 0, "NoValidMetadata"_ns, {}); 183 mInvalidMetadata = true; 184 return; // ignore invalid metadata for forward-compatibility 185 } 186 187 aMetadata.GetHashType(&mHashType, &mHashLength); 188 } 189 190 nsresult SRICheckDataVerifier::EnsureCryptoHash() { 191 MOZ_ASSERT(!mInvalidMetadata); 192 193 if (mCryptoHash) { 194 return NS_OK; 195 } 196 197 nsCOMPtr<nsICryptoHash> cryptoHash; 198 nsresult rv = NS_NewCryptoHash(mHashType, getter_AddRefs(cryptoHash)); 199 NS_ENSURE_SUCCESS(rv, rv); 200 201 mCryptoHash = cryptoHash; 202 return NS_OK; 203 } 204 205 nsresult SRICheckDataVerifier::Update(Span<const uint8_t> aBytes) { 206 return Update(aBytes.size(), aBytes.data()); 207 } 208 209 nsresult SRICheckDataVerifier::Update(uint32_t aStringLen, 210 const uint8_t* aString) { 211 NS_ENSURE_ARG_POINTER(aString); 212 if (mInvalidMetadata) { 213 return NS_OK; // ignoring any data updates, see mInvalidMetadata usage 214 } 215 216 nsresult rv; 217 rv = EnsureCryptoHash(); 218 NS_ENSURE_SUCCESS(rv, rv); 219 220 mBytesHashed += aStringLen; 221 222 return mCryptoHash->Update(aString, aStringLen); 223 } 224 225 nsresult SRICheckDataVerifier::Finish() { 226 if (mInvalidMetadata || mComplete) { 227 return NS_OK; // already finished or invalid metadata 228 } 229 230 nsresult rv; 231 rv = EnsureCryptoHash(); // we need computed hash even for 0-length data 232 NS_ENSURE_SUCCESS(rv, rv); 233 234 rv = mCryptoHash->Finish(false, mComputedHash); 235 mCryptoHash = nullptr; 236 mComplete = true; 237 return rv; 238 } 239 240 nsresult SRICheckDataVerifier::VerifyHash( 241 nsIChannel* aChannel, const SRIMetadata& aMetadata, uint32_t aHashIndex, 242 nsIConsoleReportCollector* aReporter) { 243 NS_ENSURE_ARG_POINTER(aReporter); 244 245 nsAutoCString base64Hash; 246 aMetadata.GetHash(aHashIndex, &base64Hash); 247 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u]=%s", aHashIndex, 248 base64Hash.get())); 249 250 nsAutoCString binaryHash; 251 252 // We're decoding the supplied hash twice. Trying base64 first. 253 nsresult rv = Base64Decode(base64Hash, binaryHash); 254 255 if (NS_FAILED(rv)) { 256 SRILOG( 257 ("SRICheckDataVerifier::VerifyHash, base64 decoding failed. Trying " 258 "base64url next.")); 259 FallibleTArray<uint8_t> decoded; 260 rv = Base64URLDecode(base64Hash, Base64URLDecodePaddingPolicy::Ignore, 261 decoded); 262 if (NS_FAILED(rv)) { 263 SRILOG( 264 ("SRICheckDataVerifier::VerifyHash, base64url decoding failed too. " 265 "Bailing out.")); 266 nsAutoCString referrer; 267 GetReferrerSpec(aChannel, referrer); 268 // if neither succeeded, we can bail out and warn 269 aReporter->AddConsoleReport( 270 nsIScriptError::errorFlag, "Sub-resource Integrity"_ns, 271 nsContentUtils::eSECURITY_PROPERTIES, referrer, 0, 0, 272 "InvalidIntegrityBase64"_ns, {}); 273 return NS_ERROR_SRI_CORRUPT; 274 } 275 binaryHash.Assign(reinterpret_cast<const char*>(decoded.Elements()), 276 decoded.Length()); 277 SRILOG( 278 ("SRICheckDataVerifier::VerifyHash, decoded supplied base64url hash " 279 "successfully.")); 280 } else { 281 SRILOG( 282 ("SRICheckDataVerifier::VerifyHash, decoded supplied base64 hash " 283 "successfully.")); 284 } 285 286 uint32_t hashLength; 287 int8_t hashType; 288 aMetadata.GetHashType(&hashType, &hashLength); 289 if (binaryHash.Length() != hashLength) { 290 SRILOG( 291 ("SRICheckDataVerifier::VerifyHash, supplied base64(url) hash was " 292 "incorrect length after decoding.")); 293 nsAutoCString referrer; 294 GetReferrerSpec(aChannel, referrer); 295 aReporter->AddConsoleReport(nsIScriptError::errorFlag, 296 "Sub-resource Integrity"_ns, 297 nsContentUtils::eSECURITY_PROPERTIES, referrer, 298 0, 0, "InvalidIntegrityLength"_ns, {}); 299 return NS_ERROR_SRI_CORRUPT; 300 } 301 302 // the decoded supplied hash should match our computed binary hash. 303 if (!binaryHash.Equals(mComputedHash)) { 304 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] did not match", 305 aHashIndex)); 306 return NS_ERROR_SRI_CORRUPT; 307 } 308 309 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] verified successfully", 310 aHashIndex)); 311 return NS_OK; 312 } 313 314 nsresult SRICheckDataVerifier::Verify(const SRIMetadata& aMetadata, 315 nsIChannel* aChannel, 316 nsIConsoleReportCollector* aReporter) { 317 MOZ_ASSERT(aChannel); 318 nsCOMPtr loadInfo = aChannel->LoadInfo(); 319 return Verify(aMetadata, aChannel, loadInfo->GetTainting(), aReporter); 320 } 321 322 nsresult SRICheckDataVerifier::Verify(const SRIMetadata& aMetadata, 323 nsIChannel* aChannel, 324 LoadTainting aLoadTainting, 325 nsIConsoleReportCollector* aReporter) { 326 NS_ENSURE_ARG_POINTER(aReporter); 327 328 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) { 329 nsAutoCString requestURL; 330 aChannel->GetName(requestURL); 331 SRILOG(("SRICheckDataVerifier::Verify, url=%s (length=%zu)", 332 requestURL.get(), mBytesHashed)); 333 } 334 335 nsresult rv = Finish(); 336 NS_ENSURE_SUCCESS(rv, rv); 337 338 if (NS_FAILED(IsEligible(aChannel, aLoadTainting, aReporter))) { 339 return NS_ERROR_SRI_NOT_ELIGIBLE; 340 } 341 342 if (mInvalidMetadata) { 343 return NS_OK; // ignore invalid metadata for forward-compatibility 344 } 345 346 for (uint32_t i = 0; i < aMetadata.HashCount(); i++) { 347 if (NS_SUCCEEDED(VerifyHash(aChannel, aMetadata, i, aReporter))) { 348 return NS_OK; // stop at the first valid hash 349 } 350 } 351 352 nsAutoCString alg; 353 aMetadata.GetAlgorithm(&alg); 354 355 nsCOMPtr<nsIURI> originalURI; 356 rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI)); 357 NS_ENSURE_SUCCESS(rv, rv); 358 nsAutoCString requestSpec; 359 rv = originalURI->GetSpec(requestSpec); 360 NS_ENSURE_SUCCESS(rv, rv); 361 362 nsAutoCString encodedHash; 363 rv = Base64Encode(mComputedHash, encodedHash); 364 NS_ENSURE_SUCCESS(rv, rv); 365 366 nsAutoCString referrer; 367 GetReferrerSpec(aChannel, referrer); 368 369 aReporter->AddConsoleReport( 370 nsIScriptError::errorFlag, "Sub-resource Integrity"_ns, 371 nsContentUtils::eSECURITY_PROPERTIES, referrer, 0, 0, 372 "IntegrityMismatch3"_ns, 373 {NS_ConvertUTF8toUTF16(alg), NS_ConvertUTF8toUTF16(requestSpec), 374 NS_ConvertUTF8toUTF16(encodedHash)}); 375 376 return NS_ERROR_SRI_CORRUPT; 377 } 378 379 uint32_t SRICheckDataVerifier::DataSummaryLength() { 380 MOZ_ASSERT(!mInvalidMetadata); 381 return sizeof(mHashType) + sizeof(mHashLength) + mHashLength; 382 } 383 384 uint32_t SRICheckDataVerifier::EmptyDataSummaryLength() { 385 return sizeof(int8_t) + sizeof(uint32_t); 386 } 387 388 nsresult SRICheckDataVerifier::DataSummaryLength(uint32_t aDataLen, 389 const uint8_t* aData, 390 uint32_t* length) { 391 *length = 0; 392 NS_ENSURE_ARG_POINTER(aData); 393 394 // we expect to always encode an SRI, even if it is empty or incomplete 395 if (aDataLen < EmptyDataSummaryLength()) { 396 SRILOG( 397 ("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] is too " 398 "small", 399 aDataLen)); 400 return NS_ERROR_SRI_IMPORT; 401 } 402 403 // decode the content of the buffer 404 size_t offset = sizeof(mHashType); 405 decltype(mHashLength) len = 0; 406 memcpy(&len, &aData[offset], sizeof(mHashLength)); 407 offset += sizeof(mHashLength); 408 409 SRIVERBOSE( 410 ("SRICheckDataVerifier::DataSummaryLength, header {%x, %x, %x, %x, %x, " 411 "...}", 412 aData[0], aData[1], aData[2], aData[3], aData[4])); 413 414 if (offset + len > aDataLen) { 415 SRILOG( 416 ("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] overflow " 417 "the buffer size", 418 aDataLen)); 419 SRIVERBOSE(("SRICheckDataVerifier::DataSummaryLength, offset[%u], len[%u]", 420 uint32_t(offset), uint32_t(len))); 421 return NS_ERROR_SRI_IMPORT; 422 } 423 *length = uint32_t(offset + len); 424 return NS_OK; 425 } 426 427 nsresult SRICheckDataVerifier::ImportDataSummary(uint32_t aDataLen, 428 const uint8_t* aData) { 429 MOZ_ASSERT(!mInvalidMetadata); // mHashType and mHashLength should be valid 430 MOZ_ASSERT(!mCryptoHash); // EnsureCryptoHash should not have been called 431 NS_ENSURE_ARG_POINTER(aData); 432 if (mInvalidMetadata) { 433 return NS_OK; // ignoring any data updates, see mInvalidMetadata usage 434 } 435 436 // we expect to always encode an SRI, even if it is empty or incomplete 437 if (aDataLen < DataSummaryLength()) { 438 SRILOG( 439 ("SRICheckDataVerifier::ImportDataSummary, encoded length[%u] is too " 440 "small", 441 aDataLen)); 442 return NS_ERROR_SRI_IMPORT; 443 } 444 445 SRIVERBOSE( 446 ("SRICheckDataVerifier::ImportDataSummary, header {%x, %x, %x, %x, %x, " 447 "...}", 448 aData[0], aData[1], aData[2], aData[3], aData[4])); 449 450 // decode the content of the buffer 451 size_t offset = 0; 452 decltype(mHashType) hashType; 453 memcpy(&hashType, &aData[offset], sizeof(mHashType)); 454 if (hashType != mHashType) { 455 SRILOG( 456 ("SRICheckDataVerifier::ImportDataSummary, hash type[%d] does not " 457 "match[%d]", 458 hashType, mHashType)); 459 return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE; 460 } 461 offset += sizeof(mHashType); 462 463 decltype(mHashLength) hashLength; 464 memcpy(&hashLength, &aData[offset], sizeof(mHashLength)); 465 if (hashLength != mHashLength) { 466 SRILOG( 467 ("SRICheckDataVerifier::ImportDataSummary, hash length[%d] does not " 468 "match[%d]", 469 hashLength, mHashLength)); 470 return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE; 471 } 472 offset += sizeof(mHashLength); 473 474 // copy the hash to mComputedHash, as-if we had finished streaming the bytes 475 mComputedHash.Assign(reinterpret_cast<const char*>(&aData[offset]), 476 mHashLength); 477 mCryptoHash = nullptr; 478 mComplete = true; 479 return NS_OK; 480 } 481 482 nsresult SRICheckDataVerifier::ExportDataSummary(uint32_t aDataLen, 483 uint8_t* aData) { 484 MOZ_ASSERT(!mInvalidMetadata); // mHashType and mHashLength should be valid 485 MOZ_ASSERT(mComplete); // finished streaming 486 NS_ENSURE_ARG_POINTER(aData); 487 NS_ENSURE_TRUE(aDataLen >= DataSummaryLength(), NS_ERROR_INVALID_ARG); 488 489 // serialize the hash in the buffer 490 size_t offset = 0; 491 memcpy(&aData[offset], &mHashType, sizeof(mHashType)); 492 offset += sizeof(mHashType); 493 memcpy(&aData[offset], &mHashLength, sizeof(mHashLength)); 494 offset += sizeof(mHashLength); 495 496 SRIVERBOSE( 497 ("SRICheckDataVerifier::ExportDataSummary, header {%x, %x, %x, %x, %x, " 498 "...}", 499 aData[0], aData[1], aData[2], aData[3], aData[4])); 500 501 // copy the hash to mComputedHash, as-if we had finished streaming the bytes 502 nsCharTraits<char>::copy(reinterpret_cast<char*>(&aData[offset]), 503 mComputedHash.get(), mHashLength); 504 return NS_OK; 505 } 506 507 nsresult SRICheckDataVerifier::ExportEmptyDataSummary(uint32_t aDataLen, 508 uint8_t* aData) { 509 NS_ENSURE_ARG_POINTER(aData); 510 NS_ENSURE_TRUE(aDataLen >= EmptyDataSummaryLength(), NS_ERROR_INVALID_ARG); 511 512 // serialize an unknown hash in the buffer, to be able to skip it later 513 size_t offset = 0; 514 memset(&aData[offset], 0, sizeof(mHashType)); 515 offset += sizeof(mHashType); 516 memset(&aData[offset], 0, sizeof(mHashLength)); 517 518 SRIVERBOSE( 519 ("SRICheckDataVerifier::ExportEmptyDataSummary, header {%x, %x, %x, %x, " 520 "%x, ...}", 521 aData[0], aData[1], aData[2], aData[3], aData[4])); 522 523 return NS_OK; 524 } 525 526 } // namespace mozilla::dom