Skip to content

Commit 163aea6

Browse files
committed
Bug 1928932 - Part 2 - Implement enforcements for untrusted script elements. r=smaug
Spec: https://w3c.github.io/trusted-types/dist/spec/#enforcement-in-scripts PR: w3c/trusted-types#579 Differential Revision: https://phabricator.services.mozilla.com/D251855
1 parent c7199a8 commit 163aea6

File tree

11 files changed

+159
-51
lines changed

11 files changed

+159
-51
lines changed

dom/interfaces/security/nsIContentSecurityPolicy.idl

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,12 @@ interface nsIContentSecurityPolicy : nsISerializable
160160
* @param aParserCreated If the script element was created by the HTML Parser
161161
* @param aTriggeringElement The script element of the inline resource to
162162
* hash. It can be null.
163-
* @param aContentOfPseudoScript The content of the psuedo-script to compare
164-
* to hash (and compare to the hashes listed in
165-
* the policy)
163+
* @param aSourceText The content of the script or pseudo-script to compare
164+
* to hash (and compare to the hashes listed in the
165+
* policy). For trusted inline scripts (i.e. that don't
166+
* need to go trough the Trusted Types default policy) we
167+
* retrieve the source text lazily for performance reasons
168+
* (see bug 1376651) and aSourceText is void.
166169
* @param aLineNumber The line number of the inline resource
167170
* (used for reporting)
168171
* @param aColumnNumber The column number of the inline resource
@@ -177,7 +180,7 @@ interface nsIContentSecurityPolicy : nsISerializable
177180
in boolean aParserCreated,
178181
in Element aTriggeringElement,
179182
in nsICSPEventListener aCSPEventListener,
180-
in AString aContentOfPseudoScript,
183+
in AString aSourceText,
181184
in unsigned long aLineNumber,
182185
in unsigned long aColumnNumber);
183186

dom/script/ScriptElement.cpp

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include "mozilla/dom/Document.h"
1515
#include "mozilla/dom/Element.h"
1616
#include "mozilla/dom/MutationEventBinding.h"
17+
#include "mozilla/dom/TrustedTypeUtils.h"
18+
#include "mozilla/dom/TrustedTypesConstants.h"
1719
#include "nsContentSink.h"
1820
#include "nsContentUtils.h"
1921
#include "nsGkAtoms.h"
@@ -146,18 +148,52 @@ bool ScriptElement::MaybeProcessScript() {
146148
return false;
147149
}
148150

149-
bool hasScriptContent = HasExternalScriptContent() ||
150-
nsContentUtils::HasNonEmptyTextContent(cont);
151-
if (!hasScriptContent) {
152-
// In the case of an empty, non-external classic script, there is nothing
153-
// to process. However, we must perform a microtask checkpoint afterwards,
154-
// as per https://html.spec.whatwg.org/#clean-up-after-running-script
155-
if (mKind == JS::loader::ScriptKind::eClassic && !mExternal) {
156-
nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
157-
"ScriptElement::MaybeProcessScript", []() { nsAutoMicroTask mt; }));
158-
}
151+
// https://html.spec.whatwg.org/#prepare-the-script-element
152+
// The spec says we should calculate "source text" of inline scripts at the
153+
// beginning of the "Prepare the script element" algorithm.
154+
// - If this is an inline script that is not trusted (i.e. we must execute the
155+
// Trusted Type default policy callback to obtain a trusted "source text")
156+
// then we must wrap the GetTrustedTypesCompliantInlineScriptText call in a
157+
// script runner.
158+
// - If it is an inline script that is trusted, we will actually retrieve the
159+
// "source text" lazily for performance reasons (see bug 1376651) so we just
160+
// use a void string.
161+
// - If it is an external script, we similarly just pass a void string.
162+
if (!HasExternalScriptContent() && !mIsTrusted) {
163+
// TODO: We should likely block parser if IsClassicNonAsyncDefer() returns
164+
// true but this is tricky because the default policy callback can actually
165+
// change the script type.
166+
nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
167+
"ScriptElement::MaybeProcessScript",
168+
[self = RefPtr<nsIScriptElement>(this)]()
169+
MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA {
170+
nsString sourceText;
171+
self->GetTrustedTypesCompliantInlineScriptText(sourceText);
172+
((ScriptElement*)self.get())->MaybeProcessScript(sourceText);
173+
}));
159174
return false;
160175
}
176+
return MaybeProcessScript(VoidString());
177+
}
178+
179+
bool ScriptElement::MaybeProcessScript(const nsAString& aSourceText) {
180+
nsIContent* cont = GetAsContent();
181+
if (!HasExternalScriptContent()) {
182+
bool hasInlineScriptContent =
183+
mIsTrusted ? nsContentUtils::HasNonEmptyTextContent(cont)
184+
: !aSourceText.IsEmpty();
185+
if (!hasInlineScriptContent) {
186+
// In the case of an empty, non-external classic script, there is nothing
187+
// to process. However, we must perform a microtask checkpoint afterwards,
188+
// as per https://html.spec.whatwg.org/#clean-up-after-running-script
189+
if (mKind == JS::loader::ScriptKind::eClassic && !mExternal) {
190+
nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
191+
"ScriptElement::MaybeProcessScript", []() { nsAutoMicroTask mt; }));
192+
}
193+
return false;
194+
}
195+
MOZ_ASSERT(mIsTrusted == aSourceText.IsVoid());
196+
}
161197

162198
// Check the type attribute to determine language and version. If type exists,
163199
// it trumps the deprecated 'language='.
@@ -214,7 +250,7 @@ bool ScriptElement::MaybeProcessScript() {
214250
}
215251

216252
RefPtr<ScriptLoader> loader = ownerDoc->ScriptLoader();
217-
return loader->ProcessScriptElement(this);
253+
return loader->ProcessScriptElement(this, aSourceText);
218254
}
219255

220256
bool ScriptElement::GetScriptType(nsAString& aType) {
@@ -250,3 +286,26 @@ void ScriptElement::UpdateTrustWorthiness(
250286
mIsTrusted = false;
251287
}
252288
}
289+
290+
nsresult ScriptElement::GetTrustedTypesCompliantInlineScriptText(
291+
nsString& aSourceText) {
292+
MOZ_ASSERT(!mIsTrusted);
293+
294+
RefPtr<Element> element = GetAsContent()->AsElement();
295+
nsAutoString sourceText;
296+
GetScriptText(sourceText);
297+
298+
MOZ_ASSERT(element->IsHTMLElement() || element->IsSVGElement());
299+
Maybe<nsAutoString> compliantStringHolder;
300+
constexpr nsLiteralString htmlSinkName = u"HTMLScriptElement text"_ns;
301+
constexpr nsLiteralString svgSinkName = u"SVGScriptElement text"_ns;
302+
ErrorResult error;
303+
const nsAString* compliantString =
304+
TrustedTypeUtils::GetTrustedTypesCompliantStringForTrustedScript(
305+
sourceText, element->IsHTMLElement() ? htmlSinkName : svgSinkName,
306+
kTrustedTypesOnlySinkGroup, *element, compliantStringHolder, error);
307+
if (!error.Failed()) {
308+
aSourceText.Assign(*compliantString);
309+
}
310+
return error.StealNSResult();
311+
}

dom/script/ScriptElement.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,14 @@ class ScriptElement : public nsIScriptElement, public nsStubMutationObserver {
4949

5050
virtual bool MaybeProcessScript() override;
5151

52+
virtual MOZ_CAN_RUN_SCRIPT nsresult
53+
GetTrustedTypesCompliantInlineScriptText(nsString& aSourceText) override;
54+
55+
private:
5256
// https://github.com/w3c/trusted-types/pull/579
5357
void UpdateTrustWorthiness(MutationEffectOnScript aMutationEffectOnScript);
58+
59+
bool MaybeProcessScript(const nsAString& aSourceText);
5460
};
5561

5662
} // namespace mozilla::dom

dom/script/ScriptLoadContext.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ NS_IMPL_ADDREF_INHERITED(ScriptLoadContext, JS::loader::LoadContextBase)
4949
NS_IMPL_RELEASE_INHERITED(ScriptLoadContext, JS::loader::LoadContextBase)
5050

5151
ScriptLoadContext::ScriptLoadContext(
52-
nsIScriptElement* aScriptElement /* = nullptr */)
52+
nsIScriptElement* aScriptElement /* = nullptr */,
53+
const nsAString& aSourceText /* = VoidString() */)
5354
: JS::loader::LoadContextBase(JS::loader::ContextKind::Window),
5455
mScriptMode(ScriptMode::eBlocking),
5556
mScriptFromHead(false),
@@ -65,6 +66,7 @@ ScriptLoadContext::ScriptLoadContext(
6566
mColumnNo(0),
6667
mIsPreload(false),
6768
mScriptElement(aScriptElement),
69+
mSourceText(aSourceText),
6870
mUnreportedPreloadError(NS_OK) {}
6971

7072
ScriptLoadContext::~ScriptLoadContext() {
@@ -147,7 +149,11 @@ bool ScriptLoadContext::HasScriptElement() const { return !!mScriptElement; }
147149

148150
void ScriptLoadContext::GetInlineScriptText(nsAString& aText) const {
149151
MOZ_ASSERT(mIsInline);
150-
mScriptElement->GetScriptText(aText);
152+
if (mSourceText.IsVoid()) {
153+
mScriptElement->GetScriptText(aText);
154+
} else {
155+
aText.Append(mSourceText);
156+
}
151157
}
152158

153159
void ScriptLoadContext::GetHintCharset(nsAString& aCharset) const {

dom/script/ScriptLoadContext.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ class ScriptLoadContext : public JS::loader::LoadContextBase,
141141
virtual ~ScriptLoadContext();
142142

143143
public:
144-
explicit ScriptLoadContext(nsIScriptElement* aScriptElement = nullptr);
144+
explicit ScriptLoadContext(nsIScriptElement* aScriptElement = nullptr,
145+
const nsAString& aSourceText = VoidString());
145146

146147
NS_DECL_ISUPPORTS_INHERITED
147148
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ScriptLoadContext,
@@ -320,6 +321,8 @@ class ScriptLoadContext : public JS::loader::LoadContextBase,
320321
// This is valid only for classic script and top-level module script.
321322
nsCOMPtr<nsIScriptElement> mScriptElement;
322323

324+
nsString mSourceText;
325+
323326
// For preload requests, we defer reporting errors to the console until the
324327
// request is used.
325328
nsresult mUnreportedPreloadError;

dom/script/ScriptLoader.cpp

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,7 @@ bool ScriptLoader::PreloadURIComparator::Equals(const PreloadInfo& aPi,
10241024
}
10251025

10261026
static bool CSPAllowsInlineScript(nsIScriptElement* aElement,
1027+
const nsAString& aSourceText,
10271028
const nsAString& aNonce,
10281029
Document* aDocument) {
10291030
nsCOMPtr<nsIContentSecurityPolicy> csp =
@@ -1041,7 +1042,7 @@ static bool CSPAllowsInlineScript(nsIScriptElement* aElement,
10411042
nsresult rv = csp->GetAllowsInline(
10421043
nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE,
10431044
false /* aHasUnsafeHash */, aNonce, parserCreated, element,
1044-
nullptr /* nsICSPEventListener */, VoidString(),
1045+
nullptr /* nsICSPEventListener */, aSourceText,
10451046
aElement->GetScriptLineNumber(),
10461047
aElement->GetScriptColumnNumber().oneOriginValue(), &allowInlineScript);
10471048
return NS_SUCCEEDED(rv) && allowInlineScript;
@@ -1102,15 +1103,17 @@ void ScriptLoader::NotifyObserversForCachedScript(
11021103

11031104
already_AddRefed<ScriptLoadRequest> ScriptLoader::CreateLoadRequest(
11041105
ScriptKind aKind, nsIURI* aURI, nsIScriptElement* aElement,
1105-
nsIPrincipal* aTriggeringPrincipal, CORSMode aCORSMode,
1106-
const nsAString& aNonce, RequestPriority aRequestPriority,
1107-
const SRIMetadata& aIntegrity, ReferrerPolicy aReferrerPolicy,
1108-
ParserMetadata aParserMetadata, ScriptLoadRequestType aRequestType) {
1106+
const nsAString& aScriptContent, nsIPrincipal* aTriggeringPrincipal,
1107+
CORSMode aCORSMode, const nsAString& aNonce,
1108+
RequestPriority aRequestPriority, const SRIMetadata& aIntegrity,
1109+
ReferrerPolicy aReferrerPolicy, ParserMetadata aParserMetadata,
1110+
ScriptLoadRequestType aRequestType) {
11091111
nsIURI* referrer = mDocument->GetDocumentURIAsReferrer();
11101112
RefPtr<ScriptFetchOptions> fetchOptions =
11111113
new ScriptFetchOptions(aCORSMode, aNonce, aRequestPriority,
11121114
aParserMetadata, aTriggeringPrincipal);
1113-
RefPtr<ScriptLoadContext> context = new ScriptLoadContext(aElement);
1115+
RefPtr<ScriptLoadContext> context =
1116+
new ScriptLoadContext(aElement, aScriptContent);
11141117

11151118
if (aKind == ScriptKind::eModule) {
11161119
RefPtr<ModuleLoadRequest> request = mModuleLoader->CreateTopLevel(
@@ -1199,7 +1202,8 @@ void ScriptLoader::EmulateNetworkEvents(ScriptLoadRequest* aRequest) {
11991202
}
12001203
}
12011204

1202-
bool ScriptLoader::ProcessScriptElement(nsIScriptElement* aElement) {
1205+
bool ScriptLoader::ProcessScriptElement(nsIScriptElement* aElement,
1206+
const nsAString& aSourceText) {
12031207
// We need a document to evaluate scripts.
12041208
NS_ENSURE_TRUE(mDocument, false);
12051209

@@ -1240,7 +1244,7 @@ bool ScriptLoader::ProcessScriptElement(nsIScriptElement* aElement) {
12401244
return ProcessExternalScript(aElement, scriptKind, scriptContent);
12411245
}
12421246

1243-
return ProcessInlineScript(aElement, scriptKind);
1247+
return ProcessInlineScript(aElement, scriptKind, aSourceText);
12441248
}
12451249

12461250
static ParserMetadata GetParserMetadata(nsIScriptElement* aElement) {
@@ -1347,8 +1351,8 @@ bool ScriptLoader::ProcessExternalScript(nsIScriptElement* aElement,
13471351
ParserMetadata parserMetadata = GetParserMetadata(aElement);
13481352

13491353
request = CreateLoadRequest(
1350-
aScriptKind, scriptURI, aElement, principal, ourCORSMode, nonce,
1351-
FetchPriorityToRequestPriority(fetchPriority), sriMetadata,
1354+
aScriptKind, scriptURI, aElement, VoidString(), principal, ourCORSMode,
1355+
nonce, FetchPriorityToRequestPriority(fetchPriority), sriMetadata,
13521356
referrerPolicy, parserMetadata, ScriptLoadRequestType::External);
13531357
request->GetScriptLoadContext()->mIsInline = false;
13541358
request->GetScriptLoadContext()->SetScriptMode(
@@ -1528,7 +1532,8 @@ bool ScriptLoader::ProcessExternalScript(nsIScriptElement* aElement,
15281532
}
15291533
15301534
bool ScriptLoader::ProcessInlineScript(nsIScriptElement* aElement,
1531-
ScriptKind aScriptKind) {
1535+
ScriptKind aScriptKind,
1536+
const nsAString& aSourceText) {
15321537
// Is this document sandboxed without 'allow-scripts'?
15331538
if (mDocument->HasScriptsBlockedBySandbox()) {
15341539
return false;
@@ -1538,7 +1543,7 @@ bool ScriptLoader::ProcessInlineScript(nsIScriptElement* aElement,
15381543
nsString nonce = nsContentSecurityUtils::GetIsElementNonceableNonce(*element);
15391544
15401545
// Does CSP allow this inline script to run?
1541-
if (!CSPAllowsInlineScript(aElement, nonce, mDocument)) {
1546+
if (!CSPAllowsInlineScript(aElement, aSourceText, nonce, mDocument)) {
15421547
return false;
15431548
}
15441549
@@ -1581,7 +1586,7 @@ bool ScriptLoader::ProcessInlineScript(nsIScriptElement* aElement,
15811586
// NOTE: The `nonce` as specified here is significant, because it's inherited
15821587
// by other scripts (e.g. modules created via dynamic imports).
15831588
RefPtr<ScriptLoadRequest> request = CreateLoadRequest(
1584-
aScriptKind, mDocument->GetDocumentURI(), aElement,
1589+
aScriptKind, mDocument->GetDocumentURI(), aElement, aSourceText,
15851590
mDocument->NodePrincipal(), corsMode, nonce,
15861591
FetchPriorityToRequestPriority(fetchPriority),
15871592
SRIMetadata(), // SRI doesn't apply
@@ -1684,9 +1689,6 @@ bool ScriptLoader::ProcessInlineScript(nsIScriptElement* aElement,
16841689
return true;
16851690
}
16861691
if (aElement->GetParserCreated() == NOT_FROM_PARSER) {
1687-
NS_ASSERTION(
1688-
!nsContentUtils::IsSafeToRunScript(),
1689-
"A script-inserted script is inserted without an update batch?");
16901692
RunScriptWhenSafe(request);
16911693
return false;
16921694
}
@@ -4597,13 +4599,13 @@ void ScriptLoader::PreloadURI(
45974599
// We treat speculative <script> loads as parser-inserted, because they
45984600
// come from a parser. This will also match how they should be treated
45994601
// as a normal load.
4600-
RefPtr<ScriptLoadRequest> request =
4601-
CreateLoadRequest(scriptKind, aURI, nullptr, mDocument->NodePrincipal(),
4602-
Element::StringToCORSMode(aCrossOrigin), aNonce,
4603-
requestPriority, sriMetadata, aReferrerPolicy,
4604-
aLinkPreload ? ParserMetadata::NotParserInserted
4605-
: ParserMetadata::ParserInserted,
4606-
ScriptLoadRequestType::Preload);
4602+
RefPtr<ScriptLoadRequest> request = CreateLoadRequest(
4603+
scriptKind, aURI, nullptr, VoidString(), mDocument->NodePrincipal(),
4604+
Element::StringToCORSMode(aCrossOrigin), aNonce, requestPriority,
4605+
sriMetadata, aReferrerPolicy,
4606+
aLinkPreload ? ParserMetadata::NotParserInserted
4607+
: ParserMetadata::ParserInserted,
4608+
ScriptLoadRequestType::Preload);
46074609
request->GetScriptLoadContext()->mIsInline = false;
46084610
request->GetScriptLoadContext()->mScriptFromHead = aScriptFromHead;
46094611
request->GetScriptLoadContext()->SetScriptMode(aDefer, aAsync, aLinkPreload);

dom/script/ScriptLoader.h

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,14 @@ class ScriptLoader final : public JS::loader::ScriptLoaderInterface {
215215
* In this case ScriptAvailable is guaranteed to be called at a later
216216
* point (as well as possibly ScriptEvaluated).
217217
*
218-
* @param aElement The element representing the script to be loaded and
219-
* evaluated.
218+
* @param aElement The element representing the script to be loaded and
219+
* evaluated.
220+
* @param aSourceText For inline non-trusted script, the source text after
221+
* application of the default Trusted Types policy, a void string otherwise.
222+
* See https://html.spec.whatwg.org/#prepare-the-script-element
220223
*/
221-
bool ProcessScriptElement(nsIScriptElement* aElement);
224+
bool ProcessScriptElement(nsIScriptElement* aElement,
225+
const nsAString& aSourceText);
222226

223227
/**
224228
* Gets the currently executing script. This is useful if you want to
@@ -470,9 +474,10 @@ class ScriptLoader final : public JS::loader::ScriptLoaderInterface {
470474

471475
already_AddRefed<ScriptLoadRequest> CreateLoadRequest(
472476
ScriptKind aKind, nsIURI* aURI, nsIScriptElement* aElement,
473-
nsIPrincipal* aTriggeringPrincipal, mozilla::CORSMode aCORSMode,
474-
const nsAString& aNonce, RequestPriority aRequestPriority,
475-
const SRIMetadata& aIntegrity, ReferrerPolicy aReferrerPolicy,
477+
const nsAString& aScriptContent, nsIPrincipal* aTriggeringPrincipal,
478+
mozilla::CORSMode aCORSMode, const nsAString& aNonce,
479+
RequestPriority aRequestPriority, const SRIMetadata& aIntegrity,
480+
ReferrerPolicy aReferrerPolicy,
476481
JS::loader::ParserMetadata aParserMetadata,
477482
ScriptLoadRequestType aRequestType);
478483

@@ -508,7 +513,8 @@ class ScriptLoader final : public JS::loader::ScriptLoaderInterface {
508513
bool ProcessExternalScript(nsIScriptElement* aElement, ScriptKind aScriptKind,
509514
nsIContent* aScriptContent);
510515

511-
bool ProcessInlineScript(nsIScriptElement* aElement, ScriptKind aScriptKind);
516+
bool ProcessInlineScript(nsIScriptElement* aElement, ScriptKind aScriptKind,
517+
const nsAString& aSourceText);
512518

513519
enum class CacheBehavior : uint8_t {
514520
DoNothing,

dom/script/nsIScriptElement.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,14 @@ class nsIScriptElement : public nsIScriptLoaderObserver {
252252
*/
253253
virtual nsresult FireErrorEvent() = 0;
254254

255+
/**
256+
* This must be called on scripts with mIsTrusted set to false in
257+
* order retrieve the associated aSourceText (source text after
258+
* application of the Trusted Types's default policy).
259+
*/
260+
virtual MOZ_CAN_RUN_SCRIPT nsresult
261+
GetTrustedTypesCompliantInlineScriptText(nsString& aSourceText) = 0;
262+
255263
protected:
256264
/**
257265
* Processes the script if it's in the document-tree and links to or

0 commit comments

Comments
 (0)