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/. */
9 #include "mozilla/Base64.h"
10 #include "mozilla/LoadTainting.h"
11 #include "mozilla/Logging.h"
12 #include "mozilla/Preferences.h"
13 #include "mozilla/dom/SRILogHelper.h"
14 #include "mozilla/dom/SRIMetadata.h"
15 #include "nsComponentManagerUtils.h"
16 #include "nsContentUtils.h"
17 #include "nsIChannel.h"
18 #include "nsIConsoleReportCollector.h"
19 #include "nsIScriptError.h"
21 #include "nsNetUtil.h"
22 #include "nsWhitespaceTokenizer.h"
24 #define SRIVERBOSE(args) \
25 MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Verbose, args)
26 #define SRILOG(args) \
27 MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug, args)
28 #define SRIERROR(args) \
29 MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Error, args)
31 namespace mozilla::dom
{
34 * Returns whether or not the sub-resource about to be loaded is eligible
35 * for integrity checks. If it's not, the checks will be skipped and the
36 * sub-resource will be loaded.
38 static nsresult
IsEligible(nsIChannel
* aChannel
,
39 mozilla::LoadTainting aTainting
,
40 const nsACString
& aSourceFileURI
,
41 nsIConsoleReportCollector
* aReporter
) {
42 NS_ENSURE_ARG_POINTER(aReporter
);
45 SRILOG(("SRICheck::IsEligible, null channel"));
46 return NS_ERROR_SRI_NOT_ELIGIBLE
;
49 // Was the sub-resource loaded via CORS?
50 if (aTainting
== LoadTainting::CORS
) {
51 SRILOG(("SRICheck::IsEligible, CORS mode"));
55 nsCOMPtr
<nsIURI
> finalURI
;
56 nsresult rv
= NS_GetFinalChannelURI(aChannel
, getter_AddRefs(finalURI
));
57 NS_ENSURE_SUCCESS(rv
, rv
);
58 nsCOMPtr
<nsIURI
> originalURI
;
59 rv
= aChannel
->GetOriginalURI(getter_AddRefs(originalURI
));
60 NS_ENSURE_SUCCESS(rv
, rv
);
61 nsAutoCString requestSpec
;
62 rv
= originalURI
->GetSpec(requestSpec
);
63 NS_ENSURE_SUCCESS(rv
, rv
);
65 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug
)) {
66 SRILOG(("SRICheck::IsEligible, requestURI=%s; finalURI=%s",
68 finalURI
? finalURI
->GetSpecOrDefault().get() : ""));
71 // Is the sub-resource same-origin?
72 if (aTainting
== LoadTainting::Basic
) {
73 SRILOG(("SRICheck::IsEligible, same-origin"));
76 SRILOG(("SRICheck::IsEligible, NOT same-origin"));
78 NS_ConvertUTF8toUTF16
requestSpecUTF16(requestSpec
);
79 nsTArray
<nsString
> params
;
80 params
.AppendElement(requestSpecUTF16
);
81 aReporter
->AddConsoleReport(
82 nsIScriptError::errorFlag
, "Sub-resource Integrity"_ns
,
83 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
84 "IneligibleResource"_ns
, const_cast<const nsTArray
<nsString
>&>(params
));
85 return NS_ERROR_SRI_NOT_ELIGIBLE
;
89 nsresult
SRICheck::IntegrityMetadata(const nsAString
& aMetadataList
,
90 const nsACString
& aSourceFileURI
,
91 nsIConsoleReportCollector
* aReporter
,
92 SRIMetadata
* outMetadata
) {
93 NS_ENSURE_ARG_POINTER(outMetadata
);
94 NS_ENSURE_ARG_POINTER(aReporter
);
95 MOZ_ASSERT(outMetadata
->IsEmpty()); // caller must pass empty metadata
97 NS_ConvertUTF16toUTF8
metadataList(aMetadataList
);
98 SRILOG(("SRICheck::IntegrityMetadata, metadataList=%s", metadataList
.get()));
100 // the integrity attribute is a list of whitespace-separated hashes
101 // and options so we need to look at them one by one and pick the
102 // strongest (valid) one
103 nsCWhitespaceTokenizer
tokenizer(metadataList
);
105 while (tokenizer
.hasMoreTokens()) {
106 token
= tokenizer
.nextToken();
108 SRIMetadata
metadata(token
);
109 if (metadata
.IsMalformed()) {
110 NS_ConvertUTF8toUTF16
tokenUTF16(token
);
111 nsTArray
<nsString
> params
;
112 params
.AppendElement(tokenUTF16
);
113 aReporter
->AddConsoleReport(
114 nsIScriptError::warningFlag
, "Sub-resource Integrity"_ns
,
115 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
116 "MalformedIntegrityHash"_ns
,
117 const_cast<const nsTArray
<nsString
>&>(params
));
118 } else if (!metadata
.IsAlgorithmSupported()) {
120 metadata
.GetAlgorithm(&alg
);
121 NS_ConvertUTF8toUTF16
algUTF16(alg
);
122 nsTArray
<nsString
> params
;
123 params
.AppendElement(algUTF16
);
124 aReporter
->AddConsoleReport(
125 nsIScriptError::warningFlag
, "Sub-resource Integrity"_ns
,
126 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
127 "UnsupportedHashAlg"_ns
,
128 const_cast<const nsTArray
<nsString
>&>(params
));
131 nsAutoCString alg1
, alg2
;
132 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug
)) {
133 outMetadata
->GetAlgorithm(&alg1
);
134 metadata
.GetAlgorithm(&alg2
);
136 if (*outMetadata
== metadata
) {
137 SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is the same as '%s'",
138 alg1
.get(), alg2
.get()));
139 *outMetadata
+= metadata
; // add new hash to strongest metadata
140 } else if (*outMetadata
< metadata
) {
141 SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is weaker than '%s'",
142 alg1
.get(), alg2
.get()));
143 *outMetadata
= metadata
; // replace strongest metadata with current
147 outMetadata
->mIntegrityString
= aMetadataList
;
149 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug
)) {
150 if (outMetadata
->IsValid()) {
152 outMetadata
->GetAlgorithm(&alg
);
153 SRILOG(("SRICheck::IntegrityMetadata, using a '%s' hash", alg
.get()));
154 } else if (outMetadata
->IsEmpty()) {
155 SRILOG(("SRICheck::IntegrityMetadata, no metadata"));
157 SRILOG(("SRICheck::IntegrityMetadata, no valid metadata found"));
163 //////////////////////////////////////////////////////////////
165 //////////////////////////////////////////////////////////////
166 SRICheckDataVerifier::SRICheckDataVerifier(const SRIMetadata
& aMetadata
,
167 const nsACString
& aSourceFileURI
,
168 nsIConsoleReportCollector
* aReporter
)
169 : mCryptoHash(nullptr),
173 mInvalidMetadata(false),
175 MOZ_ASSERT(!aMetadata
.IsEmpty()); // should be checked by caller
176 MOZ_ASSERT(aReporter
);
178 if (!aMetadata
.IsValid()) {
179 nsTArray
<nsString
> params
;
180 aReporter
->AddConsoleReport(
181 nsIScriptError::warningFlag
, "Sub-resource Integrity"_ns
,
182 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
183 "NoValidMetadata"_ns
, const_cast<const nsTArray
<nsString
>&>(params
));
184 mInvalidMetadata
= true;
185 return; // ignore invalid metadata for forward-compatibility
188 aMetadata
.GetHashType(&mHashType
, &mHashLength
);
191 nsresult
SRICheckDataVerifier::EnsureCryptoHash() {
192 MOZ_ASSERT(!mInvalidMetadata
);
198 nsCOMPtr
<nsICryptoHash
> cryptoHash
;
199 nsresult rv
= NS_NewCryptoHash(mHashType
, getter_AddRefs(cryptoHash
));
200 NS_ENSURE_SUCCESS(rv
, rv
);
202 mCryptoHash
= cryptoHash
;
206 nsresult
SRICheckDataVerifier::Update(uint32_t aStringLen
,
207 const uint8_t* aString
) {
208 NS_ENSURE_ARG_POINTER(aString
);
209 if (mInvalidMetadata
) {
210 return NS_OK
; // ignoring any data updates, see mInvalidMetadata usage
214 rv
= EnsureCryptoHash();
215 NS_ENSURE_SUCCESS(rv
, rv
);
217 mBytesHashed
+= aStringLen
;
219 return mCryptoHash
->Update(aString
, aStringLen
);
222 nsresult
SRICheckDataVerifier::Finish() {
223 if (mInvalidMetadata
|| mComplete
) {
224 return NS_OK
; // already finished or invalid metadata
228 rv
= EnsureCryptoHash(); // we need computed hash even for 0-length data
229 NS_ENSURE_SUCCESS(rv
, rv
);
231 rv
= mCryptoHash
->Finish(false, mComputedHash
);
232 mCryptoHash
= nullptr;
237 nsresult
SRICheckDataVerifier::VerifyHash(
238 const SRIMetadata
& aMetadata
, uint32_t aHashIndex
,
239 const nsACString
& aSourceFileURI
, nsIConsoleReportCollector
* aReporter
) {
240 NS_ENSURE_ARG_POINTER(aReporter
);
242 nsAutoCString base64Hash
;
243 aMetadata
.GetHash(aHashIndex
, &base64Hash
);
244 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u]=%s", aHashIndex
,
247 nsAutoCString binaryHash
;
249 // We're decoding the supplied hash twice. Trying base64 first.
250 nsresult rv
= Base64Decode(base64Hash
, binaryHash
);
254 ("SRICheckDataVerifier::VerifyHash, base64 decoding failed. Trying "
256 FallibleTArray
<uint8_t> decoded
;
257 rv
= Base64URLDecode(base64Hash
, Base64URLDecodePaddingPolicy::Ignore
,
261 ("SRICheckDataVerifier::VerifyHash, base64url decoding failed too. "
263 // if neither succeeded, we can bail out and warn
264 nsTArray
<nsString
> params
;
265 aReporter
->AddConsoleReport(
266 nsIScriptError::errorFlag
, "Sub-resource Integrity"_ns
,
267 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
268 "InvalidIntegrityBase64"_ns
,
269 const_cast<const nsTArray
<nsString
>&>(params
));
270 return NS_ERROR_SRI_CORRUPT
;
272 binaryHash
.Assign(reinterpret_cast<const char*>(decoded
.Elements()),
275 ("SRICheckDataVerifier::VerifyHash, decoded supplied base64url hash "
279 ("SRICheckDataVerifier::VerifyHash, decoded supplied base64 hash "
285 aMetadata
.GetHashType(&hashType
, &hashLength
);
286 if (binaryHash
.Length() != hashLength
) {
288 ("SRICheckDataVerifier::VerifyHash, supplied base64(url) hash was "
289 "incorrect length after decoding."));
290 nsTArray
<nsString
> params
;
291 aReporter
->AddConsoleReport(
292 nsIScriptError::errorFlag
, "Sub-resource Integrity"_ns
,
293 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
294 "InvalidIntegrityLength"_ns
,
295 const_cast<const nsTArray
<nsString
>&>(params
));
296 return NS_ERROR_SRI_CORRUPT
;
299 // the decoded supplied hash should match our computed binary hash.
300 if (!binaryHash
.Equals(mComputedHash
)) {
301 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] did not match",
303 return NS_ERROR_SRI_CORRUPT
;
306 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] verified successfully",
311 nsresult
SRICheckDataVerifier::Verify(const SRIMetadata
& aMetadata
,
312 nsIChannel
* aChannel
,
313 const nsACString
& aSourceFileURI
,
314 nsIConsoleReportCollector
* aReporter
) {
315 NS_ENSURE_ARG_POINTER(aReporter
);
317 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug
)) {
318 nsAutoCString requestURL
;
319 nsCOMPtr
<nsIRequest
> request
= aChannel
;
320 request
->GetName(requestURL
);
321 SRILOG(("SRICheckDataVerifier::Verify, url=%s (length=%zu)",
322 requestURL
.get(), mBytesHashed
));
325 nsresult rv
= Finish();
326 NS_ENSURE_SUCCESS(rv
, rv
);
328 nsCOMPtr
<nsILoadInfo
> loadInfo
= aChannel
->LoadInfo();
329 LoadTainting tainting
= loadInfo
->GetTainting();
331 if (NS_FAILED(IsEligible(aChannel
, tainting
, aSourceFileURI
, aReporter
))) {
332 return NS_ERROR_SRI_NOT_ELIGIBLE
;
335 if (mInvalidMetadata
) {
336 return NS_OK
; // ignore invalid metadata for forward-compatibility
339 for (uint32_t i
= 0; i
< aMetadata
.HashCount(); i
++) {
340 if (NS_SUCCEEDED(VerifyHash(aMetadata
, i
, aSourceFileURI
, aReporter
))) {
341 return NS_OK
; // stop at the first valid hash
346 aMetadata
.GetAlgorithm(&alg
);
348 nsCOMPtr
<nsIURI
> originalURI
;
349 rv
= aChannel
->GetOriginalURI(getter_AddRefs(originalURI
));
350 NS_ENSURE_SUCCESS(rv
, rv
);
351 nsAutoCString requestSpec
;
352 rv
= originalURI
->GetSpec(requestSpec
);
353 NS_ENSURE_SUCCESS(rv
, rv
);
355 nsAutoCString encodedHash
;
356 rv
= Base64Encode(mComputedHash
, encodedHash
);
357 NS_ENSURE_SUCCESS(rv
, rv
);
359 nsTArray
<nsString
> params
;
360 params
.AppendElement(NS_ConvertUTF8toUTF16(alg
));
361 params
.AppendElement(NS_ConvertUTF8toUTF16(requestSpec
));
362 params
.AppendElement(NS_ConvertUTF8toUTF16(encodedHash
));
363 aReporter
->AddConsoleReport(
364 nsIScriptError::errorFlag
, "Sub-resource Integrity"_ns
,
365 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
366 "IntegrityMismatch3"_ns
, const_cast<const nsTArray
<nsString
>&>(params
));
368 return NS_ERROR_SRI_CORRUPT
;
371 uint32_t SRICheckDataVerifier::DataSummaryLength() {
372 MOZ_ASSERT(!mInvalidMetadata
);
373 return sizeof(mHashType
) + sizeof(mHashLength
) + mHashLength
;
376 uint32_t SRICheckDataVerifier::EmptyDataSummaryLength() {
377 return sizeof(int8_t) + sizeof(uint32_t);
380 nsresult
SRICheckDataVerifier::DataSummaryLength(uint32_t aDataLen
,
381 const uint8_t* aData
,
384 NS_ENSURE_ARG_POINTER(aData
);
386 // we expect to always encode an SRI, even if it is empty or incomplete
387 if (aDataLen
< EmptyDataSummaryLength()) {
389 ("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] is too "
392 return NS_ERROR_SRI_IMPORT
;
395 // decode the content of the buffer
396 size_t offset
= sizeof(mHashType
);
397 decltype(mHashLength
) len
= 0;
398 memcpy(&len
, &aData
[offset
], sizeof(mHashLength
));
399 offset
+= sizeof(mHashLength
);
402 ("SRICheckDataVerifier::DataSummaryLength, header {%x, %x, %x, %x, %x, "
404 aData
[0], aData
[1], aData
[2], aData
[3], aData
[4]));
406 if (offset
+ len
> aDataLen
) {
408 ("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] overflow "
411 SRIVERBOSE(("SRICheckDataVerifier::DataSummaryLength, offset[%u], len[%u]",
412 uint32_t(offset
), uint32_t(len
)));
413 return NS_ERROR_SRI_IMPORT
;
415 *length
= uint32_t(offset
+ len
);
419 nsresult
SRICheckDataVerifier::ImportDataSummary(uint32_t aDataLen
,
420 const uint8_t* aData
) {
421 MOZ_ASSERT(!mInvalidMetadata
); // mHashType and mHashLength should be valid
422 MOZ_ASSERT(!mCryptoHash
); // EnsureCryptoHash should not have been called
423 NS_ENSURE_ARG_POINTER(aData
);
424 if (mInvalidMetadata
) {
425 return NS_OK
; // ignoring any data updates, see mInvalidMetadata usage
428 // we expect to always encode an SRI, even if it is empty or incomplete
429 if (aDataLen
< DataSummaryLength()) {
431 ("SRICheckDataVerifier::ImportDataSummary, encoded length[%u] is too "
434 return NS_ERROR_SRI_IMPORT
;
438 ("SRICheckDataVerifier::ImportDataSummary, header {%x, %x, %x, %x, %x, "
440 aData
[0], aData
[1], aData
[2], aData
[3], aData
[4]));
442 // decode the content of the buffer
444 decltype(mHashType
) hashType
;
445 memcpy(&hashType
, &aData
[offset
], sizeof(mHashType
));
446 if (hashType
!= mHashType
) {
448 ("SRICheckDataVerifier::ImportDataSummary, hash type[%d] does not "
450 hashType
, mHashType
));
451 return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE
;
453 offset
+= sizeof(mHashType
);
455 decltype(mHashLength
) hashLength
;
456 memcpy(&hashLength
, &aData
[offset
], sizeof(mHashLength
));
457 if (hashLength
!= mHashLength
) {
459 ("SRICheckDataVerifier::ImportDataSummary, hash length[%d] does not "
461 hashLength
, mHashLength
));
462 return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE
;
464 offset
+= sizeof(mHashLength
);
466 // copy the hash to mComputedHash, as-if we had finished streaming the bytes
467 mComputedHash
.Assign(reinterpret_cast<const char*>(&aData
[offset
]),
469 mCryptoHash
= nullptr;
474 nsresult
SRICheckDataVerifier::ExportDataSummary(uint32_t aDataLen
,
476 MOZ_ASSERT(!mInvalidMetadata
); // mHashType and mHashLength should be valid
477 MOZ_ASSERT(mComplete
); // finished streaming
478 NS_ENSURE_ARG_POINTER(aData
);
479 NS_ENSURE_TRUE(aDataLen
>= DataSummaryLength(), NS_ERROR_INVALID_ARG
);
481 // serialize the hash in the buffer
483 memcpy(&aData
[offset
], &mHashType
, sizeof(mHashType
));
484 offset
+= sizeof(mHashType
);
485 memcpy(&aData
[offset
], &mHashLength
, sizeof(mHashLength
));
486 offset
+= sizeof(mHashLength
);
489 ("SRICheckDataVerifier::ExportDataSummary, header {%x, %x, %x, %x, %x, "
491 aData
[0], aData
[1], aData
[2], aData
[3], aData
[4]));
493 // copy the hash to mComputedHash, as-if we had finished streaming the bytes
494 nsCharTraits
<char>::copy(reinterpret_cast<char*>(&aData
[offset
]),
495 mComputedHash
.get(), mHashLength
);
499 nsresult
SRICheckDataVerifier::ExportEmptyDataSummary(uint32_t aDataLen
,
501 NS_ENSURE_ARG_POINTER(aData
);
502 NS_ENSURE_TRUE(aDataLen
>= EmptyDataSummaryLength(), NS_ERROR_INVALID_ARG
);
504 // serialize an unknown hash in the buffer, to be able to skip it later
506 memset(&aData
[offset
], 0, sizeof(mHashType
));
507 offset
+= sizeof(mHashType
);
508 memset(&aData
[offset
], 0, sizeof(mHashLength
));
511 ("SRICheckDataVerifier::ExportEmptyDataSummary, header {%x, %x, %x, %x, "
513 aData
[0], aData
[1], aData
[2], aData
[3], aData
[4]));
518 } // namespace mozilla::dom