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 // put a reasonable bound on the length of the metadata
98 NS_ConvertUTF16toUTF8
metadataList(aMetadataList
);
99 if (metadataList
.Length() > SRICheck::MAX_METADATA_LENGTH
) {
100 metadataList
.Truncate(SRICheck::MAX_METADATA_LENGTH
);
102 SRILOG(("SRICheck::IntegrityMetadata, metadataList=%s", metadataList
.get()));
104 // the integrity attribute is a list of whitespace-separated hashes
105 // and options so we need to look at them one by one and pick the
106 // strongest (valid) one
107 nsCWhitespaceTokenizer
tokenizer(metadataList
);
110 tokenizer
.hasMoreTokens() && i
< SRICheck::MAX_METADATA_TOKENS
; ++i
) {
111 token
= tokenizer
.nextToken();
113 SRIMetadata
metadata(token
);
114 if (metadata
.IsMalformed()) {
115 NS_ConvertUTF8toUTF16
tokenUTF16(token
);
116 nsTArray
<nsString
> params
;
117 params
.AppendElement(tokenUTF16
);
118 aReporter
->AddConsoleReport(
119 nsIScriptError::warningFlag
, "Sub-resource Integrity"_ns
,
120 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
121 "MalformedIntegrityHash"_ns
,
122 const_cast<const nsTArray
<nsString
>&>(params
));
123 } else if (!metadata
.IsAlgorithmSupported()) {
125 metadata
.GetAlgorithm(&alg
);
126 NS_ConvertUTF8toUTF16
algUTF16(alg
);
127 nsTArray
<nsString
> params
;
128 params
.AppendElement(algUTF16
);
129 aReporter
->AddConsoleReport(
130 nsIScriptError::warningFlag
, "Sub-resource Integrity"_ns
,
131 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
132 "UnsupportedHashAlg"_ns
,
133 const_cast<const nsTArray
<nsString
>&>(params
));
136 nsAutoCString alg1
, alg2
;
137 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug
)) {
138 outMetadata
->GetAlgorithm(&alg1
);
139 metadata
.GetAlgorithm(&alg2
);
141 if (*outMetadata
== metadata
) {
142 SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is the same as '%s'",
143 alg1
.get(), alg2
.get()));
144 *outMetadata
+= metadata
; // add new hash to strongest metadata
145 } else if (*outMetadata
< metadata
) {
146 SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is weaker than '%s'",
147 alg1
.get(), alg2
.get()));
148 *outMetadata
= metadata
; // replace strongest metadata with current
152 outMetadata
->mIntegrityString
= aMetadataList
;
154 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug
)) {
155 if (outMetadata
->IsValid()) {
157 outMetadata
->GetAlgorithm(&alg
);
158 SRILOG(("SRICheck::IntegrityMetadata, using a '%s' hash", alg
.get()));
159 } else if (outMetadata
->IsEmpty()) {
160 SRILOG(("SRICheck::IntegrityMetadata, no metadata"));
162 SRILOG(("SRICheck::IntegrityMetadata, no valid metadata found"));
168 //////////////////////////////////////////////////////////////
170 //////////////////////////////////////////////////////////////
171 SRICheckDataVerifier::SRICheckDataVerifier(const SRIMetadata
& aMetadata
,
172 const nsACString
& aSourceFileURI
,
173 nsIConsoleReportCollector
* aReporter
)
174 : mCryptoHash(nullptr),
178 mInvalidMetadata(false),
180 MOZ_ASSERT(!aMetadata
.IsEmpty()); // should be checked by caller
181 MOZ_ASSERT(aReporter
);
183 if (!aMetadata
.IsValid()) {
184 nsTArray
<nsString
> params
;
185 aReporter
->AddConsoleReport(
186 nsIScriptError::warningFlag
, "Sub-resource Integrity"_ns
,
187 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
188 "NoValidMetadata"_ns
, const_cast<const nsTArray
<nsString
>&>(params
));
189 mInvalidMetadata
= true;
190 return; // ignore invalid metadata for forward-compatibility
193 aMetadata
.GetHashType(&mHashType
, &mHashLength
);
196 nsresult
SRICheckDataVerifier::EnsureCryptoHash() {
197 MOZ_ASSERT(!mInvalidMetadata
);
203 nsCOMPtr
<nsICryptoHash
> cryptoHash
;
204 nsresult rv
= NS_NewCryptoHash(mHashType
, getter_AddRefs(cryptoHash
));
205 NS_ENSURE_SUCCESS(rv
, rv
);
207 mCryptoHash
= cryptoHash
;
211 nsresult
SRICheckDataVerifier::Update(uint32_t aStringLen
,
212 const uint8_t* aString
) {
213 NS_ENSURE_ARG_POINTER(aString
);
214 if (mInvalidMetadata
) {
215 return NS_OK
; // ignoring any data updates, see mInvalidMetadata usage
219 rv
= EnsureCryptoHash();
220 NS_ENSURE_SUCCESS(rv
, rv
);
222 mBytesHashed
+= aStringLen
;
224 return mCryptoHash
->Update(aString
, aStringLen
);
227 nsresult
SRICheckDataVerifier::Finish() {
228 if (mInvalidMetadata
|| mComplete
) {
229 return NS_OK
; // already finished or invalid metadata
233 rv
= EnsureCryptoHash(); // we need computed hash even for 0-length data
234 NS_ENSURE_SUCCESS(rv
, rv
);
236 rv
= mCryptoHash
->Finish(false, mComputedHash
);
237 mCryptoHash
= nullptr;
242 nsresult
SRICheckDataVerifier::VerifyHash(
243 const SRIMetadata
& aMetadata
, uint32_t aHashIndex
,
244 const nsACString
& aSourceFileURI
, nsIConsoleReportCollector
* aReporter
) {
245 NS_ENSURE_ARG_POINTER(aReporter
);
247 nsAutoCString base64Hash
;
248 aMetadata
.GetHash(aHashIndex
, &base64Hash
);
249 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u]=%s", aHashIndex
,
252 nsAutoCString binaryHash
;
253 if (NS_WARN_IF(NS_FAILED(Base64Decode(base64Hash
, binaryHash
)))) {
254 nsTArray
<nsString
> params
;
255 aReporter
->AddConsoleReport(
256 nsIScriptError::errorFlag
, "Sub-resource Integrity"_ns
,
257 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
258 "InvalidIntegrityBase64"_ns
,
259 const_cast<const nsTArray
<nsString
>&>(params
));
260 return NS_ERROR_SRI_CORRUPT
;
265 aMetadata
.GetHashType(&hashType
, &hashLength
);
266 if (binaryHash
.Length() != hashLength
) {
267 nsTArray
<nsString
> params
;
268 aReporter
->AddConsoleReport(
269 nsIScriptError::errorFlag
, "Sub-resource Integrity"_ns
,
270 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
271 "InvalidIntegrityLength"_ns
,
272 const_cast<const nsTArray
<nsString
>&>(params
));
273 return NS_ERROR_SRI_CORRUPT
;
276 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug
)) {
277 nsAutoCString encodedHash
;
278 nsresult rv
= Base64Encode(mComputedHash
, encodedHash
);
279 if (NS_SUCCEEDED(rv
)) {
280 SRILOG(("SRICheckDataVerifier::VerifyHash, mComputedHash=%s",
285 if (!binaryHash
.Equals(mComputedHash
)) {
286 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] did not match",
288 return NS_ERROR_SRI_CORRUPT
;
291 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] verified successfully",
296 nsresult
SRICheckDataVerifier::Verify(const SRIMetadata
& aMetadata
,
297 nsIChannel
* aChannel
,
298 const nsACString
& aSourceFileURI
,
299 nsIConsoleReportCollector
* aReporter
) {
300 NS_ENSURE_ARG_POINTER(aReporter
);
302 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug
)) {
303 nsAutoCString requestURL
;
304 nsCOMPtr
<nsIRequest
> request
= aChannel
;
305 request
->GetName(requestURL
);
306 SRILOG(("SRICheckDataVerifier::Verify, url=%s (length=%zu)",
307 requestURL
.get(), mBytesHashed
));
310 nsresult rv
= Finish();
311 NS_ENSURE_SUCCESS(rv
, rv
);
313 nsCOMPtr
<nsILoadInfo
> loadInfo
= aChannel
->LoadInfo();
314 LoadTainting tainting
= loadInfo
->GetTainting();
316 if (NS_FAILED(IsEligible(aChannel
, tainting
, aSourceFileURI
, aReporter
))) {
317 return NS_ERROR_SRI_NOT_ELIGIBLE
;
320 if (mInvalidMetadata
) {
321 return NS_OK
; // ignore invalid metadata for forward-compatibility
324 for (uint32_t i
= 0; i
< aMetadata
.HashCount(); i
++) {
325 if (NS_SUCCEEDED(VerifyHash(aMetadata
, i
, aSourceFileURI
, aReporter
))) {
326 return NS_OK
; // stop at the first valid hash
331 aMetadata
.GetAlgorithm(&alg
);
332 NS_ConvertUTF8toUTF16
algUTF16(alg
);
333 nsTArray
<nsString
> params
;
334 params
.AppendElement(algUTF16
);
335 aReporter
->AddConsoleReport(
336 nsIScriptError::errorFlag
, "Sub-resource Integrity"_ns
,
337 nsContentUtils::eSECURITY_PROPERTIES
, aSourceFileURI
, 0, 0,
338 "IntegrityMismatch"_ns
, const_cast<const nsTArray
<nsString
>&>(params
));
339 return NS_ERROR_SRI_CORRUPT
;
342 uint32_t SRICheckDataVerifier::DataSummaryLength() {
343 MOZ_ASSERT(!mInvalidMetadata
);
344 return sizeof(mHashType
) + sizeof(mHashLength
) + mHashLength
;
347 uint32_t SRICheckDataVerifier::EmptyDataSummaryLength() {
348 return sizeof(int8_t) + sizeof(uint32_t);
351 nsresult
SRICheckDataVerifier::DataSummaryLength(uint32_t aDataLen
,
352 const uint8_t* aData
,
355 NS_ENSURE_ARG_POINTER(aData
);
357 // we expect to always encode an SRI, even if it is empty or incomplete
358 if (aDataLen
< EmptyDataSummaryLength()) {
360 ("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] is too "
363 return NS_ERROR_SRI_IMPORT
;
366 // decode the content of the buffer
367 size_t offset
= sizeof(mHashType
);
368 decltype(mHashLength
) len
= 0;
369 memcpy(&len
, &aData
[offset
], sizeof(mHashLength
));
370 offset
+= sizeof(mHashLength
);
373 ("SRICheckDataVerifier::DataSummaryLength, header {%x, %x, %x, %x, %x, "
375 aData
[0], aData
[1], aData
[2], aData
[3], aData
[4]));
377 if (offset
+ len
> aDataLen
) {
379 ("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] overflow "
382 SRIVERBOSE(("SRICheckDataVerifier::DataSummaryLength, offset[%u], len[%u]",
383 uint32_t(offset
), uint32_t(len
)));
384 return NS_ERROR_SRI_IMPORT
;
386 *length
= uint32_t(offset
+ len
);
390 nsresult
SRICheckDataVerifier::ImportDataSummary(uint32_t aDataLen
,
391 const uint8_t* aData
) {
392 MOZ_ASSERT(!mInvalidMetadata
); // mHashType and mHashLength should be valid
393 MOZ_ASSERT(!mCryptoHash
); // EnsureCryptoHash should not have been called
394 NS_ENSURE_ARG_POINTER(aData
);
395 if (mInvalidMetadata
) {
396 return NS_OK
; // ignoring any data updates, see mInvalidMetadata usage
399 // we expect to always encode an SRI, even if it is empty or incomplete
400 if (aDataLen
< DataSummaryLength()) {
402 ("SRICheckDataVerifier::ImportDataSummary, encoded length[%u] is too "
405 return NS_ERROR_SRI_IMPORT
;
409 ("SRICheckDataVerifier::ImportDataSummary, header {%x, %x, %x, %x, %x, "
411 aData
[0], aData
[1], aData
[2], aData
[3], aData
[4]));
413 // decode the content of the buffer
415 decltype(mHashType
) hashType
;
416 memcpy(&hashType
, &aData
[offset
], sizeof(mHashType
));
417 if (hashType
!= mHashType
) {
419 ("SRICheckDataVerifier::ImportDataSummary, hash type[%d] does not "
421 hashType
, mHashType
));
422 return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE
;
424 offset
+= sizeof(mHashType
);
426 decltype(mHashLength
) hashLength
;
427 memcpy(&hashLength
, &aData
[offset
], sizeof(mHashLength
));
428 if (hashLength
!= mHashLength
) {
430 ("SRICheckDataVerifier::ImportDataSummary, hash length[%d] does not "
432 hashLength
, mHashLength
));
433 return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE
;
435 offset
+= sizeof(mHashLength
);
437 // copy the hash to mComputedHash, as-if we had finished streaming the bytes
438 mComputedHash
.Assign(reinterpret_cast<const char*>(&aData
[offset
]),
440 mCryptoHash
= nullptr;
445 nsresult
SRICheckDataVerifier::ExportDataSummary(uint32_t aDataLen
,
447 MOZ_ASSERT(!mInvalidMetadata
); // mHashType and mHashLength should be valid
448 MOZ_ASSERT(mComplete
); // finished streaming
449 NS_ENSURE_ARG_POINTER(aData
);
450 NS_ENSURE_TRUE(aDataLen
>= DataSummaryLength(), NS_ERROR_INVALID_ARG
);
452 // serialize the hash in the buffer
454 memcpy(&aData
[offset
], &mHashType
, sizeof(mHashType
));
455 offset
+= sizeof(mHashType
);
456 memcpy(&aData
[offset
], &mHashLength
, sizeof(mHashLength
));
457 offset
+= sizeof(mHashLength
);
460 ("SRICheckDataVerifier::ExportDataSummary, header {%x, %x, %x, %x, %x, "
462 aData
[0], aData
[1], aData
[2], aData
[3], aData
[4]));
464 // copy the hash to mComputedHash, as-if we had finished streaming the bytes
465 nsCharTraits
<char>::copy(reinterpret_cast<char*>(&aData
[offset
]),
466 mComputedHash
.get(), mHashLength
);
470 nsresult
SRICheckDataVerifier::ExportEmptyDataSummary(uint32_t aDataLen
,
472 NS_ENSURE_ARG_POINTER(aData
);
473 NS_ENSURE_TRUE(aDataLen
>= EmptyDataSummaryLength(), NS_ERROR_INVALID_ARG
);
475 // serialize an unknown hash in the buffer, to be able to skip it later
477 memset(&aData
[offset
], 0, sizeof(mHashType
));
478 offset
+= sizeof(mHashType
);
479 memset(&aData
[offset
], 0, sizeof(mHashLength
));
480 offset
+= sizeof(mHashLength
);
483 ("SRICheckDataVerifier::ExportEmptyDataSummary, header {%x, %x, %x, %x, "
485 aData
[0], aData
[1], aData
[2], aData
[3], aData
[4]));
490 } // namespace mozilla::dom