From ec29404083e6c87725ad290b3a30479106bec76b Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 4 Jan 2024 04:43:00 -0600 Subject: [PATCH] Issue #2402 - Implement security policy violation event. https://bugzilla.mozilla.org/show_bug.cgi?id=1037335 --- .../security/nsIContentSecurityPolicy.idl | 2 - dom/security/nsCSPContext.cpp | 187 +++++++++++++----- dom/security/nsCSPContext.h | 44 ++++- .../SecurityPolicyViolationEvent.webidl | 41 ++++ dom/webidl/moz.build | 1 + 5 files changed, 215 insertions(+), 60 deletions(-) create mode 100644 dom/webidl/SecurityPolicyViolationEvent.webidl diff --git a/dom/interfaces/security/nsIContentSecurityPolicy.idl b/dom/interfaces/security/nsIContentSecurityPolicy.idl index e76c39c44b..a8b508ce7e 100644 --- a/dom/interfaces/security/nsIContentSecurityPolicy.idl +++ b/dom/interfaces/security/nsIContentSecurityPolicy.idl @@ -6,11 +6,9 @@ #include "nsIContentPolicy.idl" interface nsIURI; -interface nsIChannel; interface nsIDocShell; interface nsIDOMDocument; interface nsIPrincipal; -interface nsIURI; /** * nsIContentSecurityPolicy diff --git a/dom/security/nsCSPContext.cpp b/dom/security/nsCSPContext.cpp index 8cc83a6d97..48245a1f7c 100644 --- a/dom/security/nsCSPContext.cpp +++ b/dom/security/nsCSPContext.cpp @@ -799,31 +799,16 @@ StripURIForReporting(nsIURI* aURI, aURI->GetSpecIgnoringRef(outStrippedURI); } -/** - * Sends CSP violation reports to all sources listed under report-uri. - * - * @param aBlockedContentSource - * Either a CSP Source (like 'self', as string) or nsIURI: the source - * of the violation. - * @param aOriginalUri - * The original URI if the blocked content is a redirect, else null - * @param aViolatedDirective - * the directive that was violated (string). - * @param aSourceFile - * name of the file containing the inline script violation - * @param aScriptSample - * a sample of the violating inline script - * @param aLineNum - * source line number of the violation (if available) - */ nsresult -nsCSPContext::SendReports(nsISupports* aBlockedContentSource, - nsIURI* aOriginalURI, - nsAString& aViolatedDirective, - uint32_t aViolatedPolicyIndex, - nsAString& aSourceFile, - nsAString& aScriptSample, - uint32_t aLineNum) +nsCSPContext::GatherSecurityPolicyViolationEventData( + nsISupports* aBlockedContentSource, + nsIURI* aOriginalURI, + nsAString& aViolatedDirective, + uint32_t aViolatedPolicyIndex, + nsAString& aSourceFile, + nsAString& aScriptSample, + uint32_t aLineNum, + mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit) { NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1); @@ -837,9 +822,16 @@ nsCSPContext::SendReports(nsISupports* aBlockedContentSource, return NS_OK; } - dom::CSPReport report; nsresult rv; + // document-uri + nsAutoCString reportDocumentURI; + StripURIForReporting(mSelfURI, mSelfURI, reportDocumentURI); + aViolationEventInit.mDocumentURI = NS_ConvertUTF8toUTF16(reportDocumentURI); + + // referrer + aViolationEventInit.mReferrer = mReferrer; + // blocked-uri if (aBlockedContentSource) { nsAutoCString reportBlockedURI; @@ -858,27 +850,20 @@ nsCSPContext::SendReports(nsISupports* aBlockedContentSource, // ancestor is cross-origin. NS_WARNING("No blocked URI (null aBlockedContentSource) for CSP violation report."); } - report.mCsp_report.mBlocked_uri = NS_ConvertUTF8toUTF16(reportBlockedURI); + aViolationEventInit.mBlockedURI = NS_ConvertUTF8toUTF16(reportBlockedURI); } - // document-uri - nsAutoCString reportDocumentURI; - StripURIForReporting(mSelfURI, mSelfURI, reportDocumentURI); - report.mCsp_report.mDocument_uri = NS_ConvertUTF8toUTF16(reportDocumentURI); + // violated-directive + aViolationEventInit.mViolatedDirective = aViolatedDirective; + + // effective-directive + aViolationEventInit.mEffectiveDirective = aViolatedDirective; // original-policy nsAutoString originalPolicy; rv = this->GetPolicyString(aViolatedPolicyIndex, originalPolicy); NS_ENSURE_SUCCESS(rv, rv); - report.mCsp_report.mOriginal_policy = originalPolicy; - - // referrer - if (!mReferrer.IsEmpty()) { - report.mCsp_report.mReferrer = mReferrer; - } - - // violated-directive - report.mCsp_report.mViolated_directive = aViolatedDirective; + aViolationEventInit.mOriginalPolicy = originalPolicy; // source-file if (!aSourceFile.IsEmpty()) { @@ -890,20 +875,87 @@ nsCSPContext::SendReports(nsISupports* aBlockedContentSource, sourceURI->GetSpecIgnoringRef(spec); aSourceFile = NS_ConvertUTF8toUTF16(spec); } + aViolationEventInit.mSourceFile = aSourceFile; + } + + // sample + aViolationEventInit.mSample = aScriptSample; + + // disposition + aViolationEventInit.mDisposition = mPolicies[aViolatedPolicyIndex]->getReportOnlyFlag() + ? mozilla::dom::SecurityPolicyViolationEventDisposition::Report + : mozilla::dom::SecurityPolicyViolationEventDisposition::Enforce; + + // status-code + uint16_t statusCode = 0; + { + nsCOMPtr doc = do_QueryReferent(mLoadingContext); + if (doc) { + nsCOMPtr channel = do_QueryInterface(doc->GetChannel()); + if (channel) { + uint32_t responseStatus = 0; + nsresult rv = channel->GetResponseStatus(&responseStatus); + if (NS_SUCCEEDED(rv) && (responseStatus <= UINT16_MAX)) { + statusCode = static_cast(responseStatus); + } + } + } + } + aViolationEventInit.mStatusCode = statusCode; + + // line-number + aViolationEventInit.mLineNumber = aLineNum; + + // column-number + // TODO: Set correct column number. + aViolationEventInit.mColumnNumber = 0; + + aViolationEventInit.mBubbles = true; + aViolationEventInit.mComposed = true; + + return NS_OK; +} + +nsresult +nsCSPContext::SendReports( + const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit, + uint32_t aViolatedPolicyIndex) +{ + NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1); + + dom::CSPReport report; + + // blocked-uri + report.mCsp_report.mBlocked_uri = aViolationEventInit.mBlockedURI; + + // document-uri + report.mCsp_report.mDocument_uri = aViolationEventInit.mDocumentURI; + + // original-policy + report.mCsp_report.mOriginal_policy = aViolationEventInit.mOriginalPolicy; + + // referrer + report.mCsp_report.mReferrer = aViolationEventInit.mReferrer; + + // violated-directive + report.mCsp_report.mViolated_directive = aViolationEventInit.mViolatedDirective; + + // source-file + if (!aViolationEventInit.mSourceFile.IsEmpty()) { report.mCsp_report.mSource_file.Construct(); - report.mCsp_report.mSource_file.Value() = aSourceFile; + report.mCsp_report.mSource_file.Value() = aViolationEventInit.mSourceFile; } // script-sample - if (!aScriptSample.IsEmpty()) { + if (!aViolationEventInit.mSample.IsEmpty()) { report.mCsp_report.mScript_sample.Construct(); - report.mCsp_report.mScript_sample.Value() = aScriptSample; + report.mCsp_report.mScript_sample.Value() = aViolationEventInit.mSample; } // line-number - if (aLineNum != 0) { + if (aViolationEventInit.mLineNumber != 0) { report.mCsp_report.mLine_number.Construct(); - report.mCsp_report.mLine_number.Value() = aLineNum; + report.mCsp_report.mLine_number.Value() = aViolationEventInit.mLineNumber; } nsString csp_report; @@ -916,11 +968,11 @@ nsCSPContext::SendReports(nsISupports* aBlockedContentSource, nsTArray reportURIs; mPolicies[aViolatedPolicyIndex]->getReportURIs(reportURIs); - nsCOMPtr doc = do_QueryReferent(mLoadingContext); nsCOMPtr reportURI; nsCOMPtr reportChannel; + nsresult rv; for (uint32_t r = 0; r < reportURIs.Length(); r++) { nsAutoCString reportURICstring = NS_ConvertUTF16toUTF8(reportURIs[r]); // try to create a new uri from every report-uri string @@ -930,7 +982,8 @@ nsCSPContext::SendReports(nsISupports* aBlockedContentSource, CSPCONTEXTLOG(("Could not create nsIURI for report URI %s", reportURICstring.get())); logToConsole(u"triedToSendReport", params, ArrayLength(params), - aSourceFile, aScriptSample, aLineNum, 0, nsIScriptError::errorFlag); + aViolationEventInit.mSourceFile, aViolationEventInit.mSample, + aViolationEventInit.mLineNumber, 0, nsIScriptError::errorFlag); continue; // don't return yet, there may be more URIs } @@ -971,7 +1024,8 @@ nsCSPContext::SendReports(nsISupports* aBlockedContentSource, if (!isHttpScheme) { const char16_t* params[] = { reportURIs[r].get() }; logToConsole(u"reportURInotHttpsOrHttp2", params, ArrayLength(params), - aSourceFile, aScriptSample, aLineNum, 0, nsIScriptError::errorFlag); + aViolationEventInit.mSourceFile, aViolationEventInit.mSample, + aViolationEventInit.mLineNumber, 0, nsIScriptError::errorFlag); continue; } @@ -1035,7 +1089,8 @@ nsCSPContext::SendReports(nsISupports* aBlockedContentSource, const char16_t* params[] = { reportURIs[r].get() }; CSPCONTEXTLOG(("AsyncOpen failed for report URI %s", params[0])); logToConsole(u"triedToSendReport", params, ArrayLength(params), - aSourceFile, aScriptSample, aLineNum, 0, nsIScriptError::errorFlag); + aViolationEventInit.mSourceFile, aViolationEventInit.mSample, + aViolationEventInit.mLineNumber, 0, nsIScriptError::errorFlag); } else { CSPCONTEXTLOG(("Sent violation report to URI %s", reportURICstring.get())); } @@ -1043,6 +1098,26 @@ nsCSPContext::SendReports(nsISupports* aBlockedContentSource, return NS_OK; } +nsresult +nsCSPContext::FireViolationEvent( + const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit) +{ + nsCOMPtr doc = do_QueryReferent(mLoadingContext); + if (!doc) { + return NS_OK; + } + + RefPtr event = + mozilla::dom::SecurityPolicyViolationEvent::Constructor( + doc, + NS_LITERAL_STRING("securitypolicyviolation"), + aViolationEventInit); + event->SetTrusted(true); + + bool rv; + return doc->DispatchEvent(event, &rv); +} + /** * Dispatched from the main thread to send reports for one CSP violation. */ @@ -1088,6 +1163,14 @@ class CSPReportSenderRunnable final : public Runnable { MOZ_ASSERT(NS_IsMainThread()); + // 0) prepare violation data + mozilla::dom::SecurityPolicyViolationEventInit init; + mCSPContext->GatherSecurityPolicyViolationEventData( + mBlockedContentSource, mOriginalURI, + mViolatedDirective, mViolatedPolicyIndex, + mSourceFile, mScriptSample, mLineNum, + init); + // 1) notify observers nsCOMPtr observerService = mozilla::services::GetObserverService(); NS_ASSERTION(observerService, "needs observer service"); @@ -1097,9 +1180,7 @@ class CSPReportSenderRunnable final : public Runnable NS_ENSURE_SUCCESS(rv, rv); // 2) send reports for the policy that was violated - mCSPContext->SendReports(mBlockedContentSource, mOriginalURI, - mViolatedDirective, mViolatedPolicyIndex, - mSourceFile, mScriptSample, mLineNum); + mCSPContext->SendReports(init, mViolatedPolicyIndex); // 3) log to console (one per policy violation) // mBlockedContentSource could be a URI or a string. @@ -1130,6 +1211,10 @@ class CSPReportSenderRunnable final : public Runnable params, ArrayLength(params), mSourceFile, mScriptSample, mLineNum, 0, nsIScriptError::errorFlag); } + + // 4) fire violation event + mCSPContext->FireViolationEvent(init); + return NS_OK; } diff --git a/dom/security/nsCSPContext.h b/dom/security/nsCSPContext.h index 3530c74c5c..8a283c1a10 100644 --- a/dom/security/nsCSPContext.h +++ b/dom/security/nsCSPContext.h @@ -7,6 +7,7 @@ #define nsCSPContext_h___ #include "mozilla/dom/nsCSPUtils.h" +#include "mozilla/dom/SecurityPolicyViolationEvent.h" #include "nsDataHashtable.h" #include "nsIChannel.h" #include "nsIChannelEventSink.h" @@ -56,13 +57,42 @@ class nsCSPContext : public nsIContentSecurityPolicy uint32_t aColumnNumber, uint32_t aSeverityFlag); - nsresult SendReports(nsISupports* aBlockedContentSource, - nsIURI* aOriginalURI, - nsAString& aViolatedDirective, - uint32_t aViolatedPolicyIndex, - nsAString& aSourceFile, - nsAString& aScriptSample, - uint32_t aLineNum); + + /** + * Construct SecurityPolicyViolationEventInit structure. + * + * @param aBlockedContentSource + * Either a CSP Source (like 'self', as string) or nsIURI: the source + * of the violation. + * @param aOriginalUri + * The original URI if the blocked content is a redirect, else null + * @param aViolatedDirective + * the directive that was violated (string). + * @param aSourceFile + * name of the file containing the inline script violation + * @param aScriptSample + * a sample of the violating inline script + * @param aLineNum + * source line number of the violation (if available) + * @param aViolationEventInit + * The output + */ + nsresult GatherSecurityPolicyViolationEventData( + nsISupports* aBlockedContentSource, + nsIURI* aOriginalURI, + nsAString& aViolatedDirective, + uint32_t aViolatedPolicyIndex, + nsAString& aSourceFile, + nsAString& aScriptSample, + uint32_t aLineNum, + mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit); + + nsresult SendReports( + const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit, + uint32_t aViolatedPolicyIndex); + + nsresult FireViolationEvent( + const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit); nsresult AsyncReportViolation(nsISupports* aBlockedContentSource, nsIURI* aOriginalURI, diff --git a/dom/webidl/SecurityPolicyViolationEvent.webidl b/dom/webidl/SecurityPolicyViolationEvent.webidl new file mode 100644 index 0000000000..0f3d5db09e --- /dev/null +++ b/dom/webidl/SecurityPolicyViolationEvent.webidl @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +enum SecurityPolicyViolationEventDisposition +{ + "enforce", "report" +}; + +[Constructor(DOMString type, optional SecurityPolicyViolationEventInit eventInitDict)] +interface SecurityPolicyViolationEvent : Event +{ + readonly attribute DOMString documentURI; + readonly attribute DOMString referrer; + readonly attribute DOMString blockedURI; + readonly attribute DOMString violatedDirective; + readonly attribute DOMString effectiveDirective; + readonly attribute DOMString originalPolicy; + readonly attribute DOMString sourceFile; + readonly attribute DOMString sample; + readonly attribute SecurityPolicyViolationEventDisposition disposition; + readonly attribute unsigned short statusCode; + readonly attribute long lineNumber; + readonly attribute long columnNumber; +}; + +dictionary SecurityPolicyViolationEventInit : EventInit +{ + DOMString documentURI = ""; + DOMString referrer = ""; + DOMString blockedURI = ""; + DOMString violatedDirective = ""; + DOMString effectiveDirective = ""; + DOMString originalPolicy = ""; + DOMString sourceFile = ""; + DOMString sample = ""; + SecurityPolicyViolationEventDisposition disposition = "report"; + unsigned short statusCode = 0; + long lineNumber = 0; + long columnNumber = 0; +}; \ No newline at end of file diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index c1f5ad1b76..e7cc11cecc 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -698,6 +698,7 @@ GENERATED_EVENTS_WEBIDL_FILES = [ 'PromiseRejectionEvent.webidl', 'RecordErrorEvent.webidl', 'ScrollViewChangeEvent.webidl', + 'SecurityPolicyViolationEvent.webidl', 'ServiceWorkerMessageEvent.webidl', 'StyleRuleChangeEvent.webidl', 'StyleSheetApplicableStateChangeEvent.webidl',