/* 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/. */ #include "ScriptElement.h" #include "ScriptLoader.h" #include "mozilla/BasicEvents.h" #include "mozilla/CycleCollectedJSContext.h" #include "mozilla/EventDispatcher.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/TrustedTypeUtils.h" #include "mozilla/dom/TrustedTypesConstants.h" #include "mozilla/extensions/WebExtensionPolicy.h" #include "nsContentSink.h" #include "nsContentUtils.h" #include "nsGkAtoms.h" #include "nsIMutationObserver.h" #include "nsIParser.h" #include "nsPresContext.h" #include "nsThreadUtils.h" using namespace mozilla; using namespace mozilla::dom; NS_IMETHODIMP ScriptElement::ScriptAvailable(nsresult aResult, nsIScriptElement* aElement, bool aIsInlineClassicScript, nsIURI* aURI, uint32_t aLineNo) { if (!aIsInlineClassicScript && NS_FAILED(aResult)) { nsCOMPtr parser = do_QueryReferent(mCreatorParser); if (parser) { nsCOMPtr sink = parser->GetContentSink(); if (sink) { nsCOMPtr parserDoc = do_QueryInterface(sink->GetTarget()); if (GetAsContent()->OwnerDoc() != parserDoc) { // Suppress errors when we've moved between docs. // /html/semantics/scripting-1/the-script-element/moving-between-documents/move-back-iframe-fetch-error-external-module.html // See also https://bugzilla.mozilla.org/show_bug.cgi?id=1849107 return NS_OK; } } } if (parser) { parser->IncrementScriptNestingLevel(); } nsresult rv = FireErrorEvent(); if (parser) { parser->DecrementScriptNestingLevel(); } return rv; } return NS_OK; } /* virtual */ nsresult ScriptElement::FireErrorEvent() { nsIContent* cont = GetAsContent(); return nsContentUtils::DispatchTrustedEvent( cont->OwnerDoc(), cont, u"error"_ns, CanBubble::eNo, Cancelable::eNo); } NS_IMETHODIMP ScriptElement::ScriptEvaluated(nsresult aResult, nsIScriptElement* aElement, bool aIsInline) { nsresult rv = NS_OK; if (!aIsInline) { nsCOMPtr cont = GetAsContent(); RefPtr presContext = nsContentUtils::GetContextForContent(cont); nsEventStatus status = nsEventStatus_eIgnore; EventMessage message = NS_SUCCEEDED(aResult) ? eLoad : eLoadError; WidgetEvent event(true, message); // Load event doesn't bubble. event.mFlags.mBubbles = (message != eLoad); EventDispatcher::Dispatch(cont, presContext, &event, nullptr, &status); } return rv; } void ScriptElement::CharacterDataChanged(nsIContent* aContent, const CharacterDataChangeInfo& aInfo) { if (!nsContentUtils::IsInSameAnonymousTree(GetAsContent(), aContent)) { return; } UpdateTrustWorthiness(aInfo.mMutationEffectOnScript); MaybeProcessScript(nullptr /* aParser */); } void ScriptElement::AttributeChanged(Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute, AttrModType aModType, const nsAttrValue* aOldValue) { if (aElement != GetAsContent()) { return; } // https://html.spec.whatwg.org/#script-processing-model // When a script element el that is not parser-inserted experiences one of the // events listed in the following list, the user agent must immediately // prepare the script element el: // - The script element is connected and has a src attribute set where // previously the element had no such attribute. if (aElement->IsSVGElement() && ((aNameSpaceID != kNameSpaceID_XLink && aNameSpaceID != kNameSpaceID_None) || aAttribute != nsGkAtoms::href)) { return; } if (aElement->IsHTMLElement() && (aNameSpaceID != kNameSpaceID_None || aAttribute != nsGkAtoms::src)) { return; } if (mParserCreated == NOT_FROM_PARSER && aModType == AttrModType::Addition && aElement->IsInComposedDoc()) { MaybeProcessScript(nullptr /* aParser */); } } void ScriptElement::ContentAppended(nsIContent* aFirstNewContent, const ContentAppendInfo& aInfo) { if (!nsContentUtils::IsInSameAnonymousTree(GetAsContent(), aFirstNewContent)) { return; } UpdateTrustWorthiness(aInfo.mMutationEffectOnScript); MaybeProcessScript(nullptr /* aParser */); } void ScriptElement::ContentInserted(nsIContent* aChild, const ContentInsertInfo& aInfo) { if (!nsContentUtils::IsInSameAnonymousTree(GetAsContent(), aChild)) { return; } UpdateTrustWorthiness(aInfo.mMutationEffectOnScript); MaybeProcessScript(nullptr /* aParser */); } void ScriptElement::ContentWillBeRemoved(nsIContent* aChild, const ContentRemoveInfo& aInfo) { if (!nsContentUtils::IsInSameAnonymousTree(GetAsContent(), aChild)) { return; } UpdateTrustWorthiness(aInfo.mMutationEffectOnScript); } bool ScriptElement::MaybeProcessScript(nsCOMPtr aParser) { nsIContent* cont = GetAsContent(); NS_ASSERTION(cont->DebugGetSlots()->mMutationObservers.contains(this), "You forgot to add self as observer"); if (mAlreadyStarted || !mDoneAddingChildren || !cont->GetComposedDoc() || mMalformed) { return false; } // https://html.spec.whatwg.org/#prepare-the-script-element // The spec says we should calculate "source text" of inline scripts at the // beginning of the "Prepare the script element" algorithm. if (HasExternalScriptContent() || mIsTrusted || TrustedTypeUtils::CanSkipTrustedTypesEnforcement( *GetAsContent()->AsElement())) { // - If it is an inline script that is trusted, we will actually retrieve // the "source text" lazily for performance reasons (see bug 1376651) so we // just pass a void string to MaybeProcessScript(). // - If it is an external script, we actually don't need the "source text" // and can similarly pass a void string to MaybeProcessScript(). bool block = MaybeProcessScript(VoidString()); if (block && aParser) { aParser->BlockParser(); } return block; } // This is an inline script that is not trusted (i.e. we must execute the // Trusted Type default policy callback to obtain a trusted "source text"). if (nsContentUtils::IsSafeToRunScript()) { // - If it is safe to run script in this context, we run the default policy // callback and pass the returned "source text" to MaybeProcessScript(). bool block = ([self = RefPtr(this)]() MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA { nsString sourceText; self->GetTrustedTypesCompliantInlineScriptText(sourceText); return static_cast(self.get()) ->MaybeProcessScript(sourceText); })(); if (block && aParser) { aParser->BlockParser(); } return block; } // - The default policy callback must be wrapped in a script runner. So we // need to block the parser at least until we can get the "source text". nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( "ScriptElement::MaybeProcessScript", [self = RefPtr(this), aParser]() MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA { nsString sourceText; self->GetTrustedTypesCompliantInlineScriptText(sourceText); bool block = static_cast(self.get()) ->MaybeProcessScript(sourceText); if (!block && aParser) { aParser->UnblockParser(); } })); if (aParser) { aParser->BlockParser(); } return true; } bool ScriptElement::MaybeProcessScript(const nsAString& aSourceText) { nsIContent* cont = GetAsContent(); if (!HasExternalScriptContent()) { // If el has no src attribute, and source text is the empty string, then // return (https://html.spec.whatwg.org/#prepare-the-script-element). // // A void aSourceText means we want to retrieve it lazily (bug 1376651), in // that case we browse the subtree to try and find a non-empty text node. bool hasInlineScriptContent = aSourceText.IsVoid() ? nsContentUtils::HasNonEmptyTextContent(cont) : !aSourceText.IsEmpty(); if (!hasInlineScriptContent) { // In the case of an empty, non-external classic script, there is nothing // to process. However, we must perform a microtask checkpoint afterwards, // as per https://html.spec.whatwg.org/#clean-up-after-running-script if (mKind == JS::loader::ScriptKind::eClassic && !mExternal) { nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( "ScriptElement::MaybeProcessScript", []() { nsAutoMicroTask mt; })); } return false; } } // Check the type attribute to determine language and version. If type exists, // it trumps the deprecated 'language='. nsAutoString type; bool hasType = GetScriptType(type); if (!type.IsEmpty()) { if (!nsContentUtils::IsJavascriptMIMEType(type) && !type.LowerCaseEqualsASCII("module") && !type.LowerCaseEqualsASCII("importmap")) { #ifdef DEBUG // There is a WebGL convention to store strings they need inside script // tags with these specific unknown script types, so don't warn for them. // "text/something-not-javascript" only seems to be used in the WebGL // conformance tests, but it is also clearly deliberately invalid, so // skip warning for it, too, to reduce warning spam. if (!type.LowerCaseEqualsASCII("x-shader/x-vertex") && !type.LowerCaseEqualsASCII("x-shader/x-fragment") && !type.LowerCaseEqualsASCII("text/something-not-javascript")) { NS_WARNING(nsPrintfCString("Unknown script type '%s'", NS_ConvertUTF16toUTF8(type).get()) .get()); } #endif // #ifdef DEBUG return false; } } else if (!hasType) { // "language" is a deprecated attribute of HTML, so we check it only for // HTML script elements. if (cont->IsHTMLElement()) { nsAutoString language; cont->AsElement()->GetAttr(nsGkAtoms::language, language); if (!language.IsEmpty() && !nsContentUtils::IsJavaScriptLanguage(language)) { return false; } } } Document* ownerDoc = cont->OwnerDoc(); FreezeExecutionAttrs(ownerDoc); mAlreadyStarted = true; nsCOMPtr parser = ((nsIScriptElement*)this)->GetCreatorParser(); if (parser) { nsCOMPtr sink = parser->GetContentSink(); if (sink) { nsCOMPtr parserDoc = do_QueryInterface(sink->GetTarget()); if (ownerDoc != parserDoc) { // Refactor this: https://bugzilla.mozilla.org/show_bug.cgi?id=1849107 return false; } } } RefPtr loader = ownerDoc->GetScriptLoader(); if (!loader) { return false; } return loader->ProcessScriptElement(this, aSourceText); } bool ScriptElement::GetScriptType(nsAString& aType) { Element* element = GetAsContent()->AsElement(); nsAutoString type; if (!element->GetAttr(nsGkAtoms::type, type)) { return false; } // ASCII whitespace https://infra.spec.whatwg.org/#ascii-whitespace: // U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE. static const char kASCIIWhitespace[] = "\t\n\f\r "; const bool wasEmptyBeforeTrim = type.IsEmpty(); type.Trim(kASCIIWhitespace); // If the value before trim was not empty and the value is now empty, do not // trim as we want to retain pure whitespace (by restoring original value) // because we need to treat "" and " " (etc) differently. if (!wasEmptyBeforeTrim && type.IsEmpty()) { return element->GetAttr(nsGkAtoms::type, aType); } aType.Assign(type); return true; } void ScriptElement::UpdateTrustWorthiness( MutationEffectOnScript aMutationEffectOnScript) { if (aMutationEffectOnScript == MutationEffectOnScript::DropTrustWorthiness && StaticPrefs::dom_security_trusted_types_enabled()) { nsCOMPtr subjectPrincipal; if (JSContext* cx = nsContentUtils::GetCurrentJSContext()) { subjectPrincipal = nsContentUtils::SubjectPrincipal(cx); if (auto* principal = BasePrincipal::Cast(subjectPrincipal)) { if (principal->IsSystemPrincipal() || principal->ContentScriptAddonPolicyCore()) { // This script was modified by a priviledged scripts, so continue to // consider it as trusted. return; } } } mIsTrusted = false; } } nsresult ScriptElement::GetTrustedTypesCompliantInlineScriptText( nsString& aSourceText) { MOZ_ASSERT(!mIsTrusted); RefPtr element = GetAsContent()->AsElement(); nsAutoString sourceText; GetScriptText(sourceText); MOZ_ASSERT(element->IsHTMLElement() || element->IsSVGElement()); Maybe compliantStringHolder; constexpr nsLiteralString htmlSinkName = u"HTMLScriptElement text"_ns; constexpr nsLiteralString svgSinkName = u"SVGScriptElement text"_ns; ErrorResult error; const nsAString* compliantString = TrustedTypeUtils::GetTrustedTypesCompliantStringForTrustedScript( sourceText, element->IsHTMLElement() ? htmlSinkName : svgSinkName, kTrustedTypesOnlySinkGroup, *element, compliantStringHolder, error); if (!error.Failed()) { aSourceText.Assign(*compliantString); } return error.StealNSResult(); }