diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index 74cd573f53f9d..c1e6003c83a6d 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -2652,7 +2652,7 @@ Future _downloadCanvasKitJs() { final String canvasKitJavaScriptUrl = canvasKitJavaScriptBindingsUrl; final DomHTMLScriptElement canvasKitScript = createDomHTMLScriptElement(); - canvasKitScript.src = canvasKitJavaScriptUrl; + canvasKitScript.src = createTrustedScriptUrl(canvasKitJavaScriptUrl); final Completer canvasKitLoadCompleter = Completer(); late DomEventListener callback; diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 3ef7e820f91c7..466216996dc96 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -62,6 +62,10 @@ extension DomWindowExtension on DomWindow { targetOrigin, if (messagePorts != null) js_util.jsify(messagePorts) ]); + + /// The Trusted Types API (when available). + /// See: https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API + external DomTrustedTypePolicyFactory? get trustedTypes; } typedef DomRequestAnimationFrameCallback = void Function(num highResTime); @@ -72,6 +76,7 @@ class DomConsole {} extension DomConsoleExtension on DomConsole { external void warn(Object? arg); + external void error(Object? arg); } @JS('window') @@ -516,7 +521,7 @@ extension DomHTMLImageElementExtension on DomHTMLImageElement { class DomHTMLScriptElement extends DomHTMLElement {} extension DomHTMLScriptElementExtension on DomHTMLScriptElement { - external set src(String value); + external set src(Object /* String|TrustedScriptURL */ value); } DomHTMLScriptElement createDomHTMLScriptElement() => @@ -1439,6 +1444,127 @@ extension DomCSSRuleListExtension on DomCSSRuleList { js_util.getProperty(this, 'length').toInt(); } +/// A factory to create `TrustedTypePolicy` objects. +/// See: https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicyFactory +@JS() +@staticInterop +abstract class DomTrustedTypePolicyFactory {} + +/// A subset of TrustedTypePolicyFactory methods. +extension DomTrustedTypePolicyFactoryExtension on DomTrustedTypePolicyFactory { + /// Creates a TrustedTypePolicy object named `policyName` that implements the + /// rules passed as `policyOptions`. + external DomTrustedTypePolicy createPolicy( + String policyName, + DomTrustedTypePolicyOptions? policyOptions, + ); +} + +/// Options to create a trusted type policy. +/// +/// The options are user-defined functions for converting strings into trusted +/// values. +/// +/// See: https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicyFactory/createPolicy#policyoptions +@JS() +@staticInterop +@anonymous +abstract class DomTrustedTypePolicyOptions { + /// Constructs a TrustedTypePolicyOptions object in JavaScript. + /// + /// `createScriptURL` is a callback function that contains code to run when + /// creating a TrustedScriptURL object. + /// + /// The following properties need to be manually wrapped in [allowInterop] + /// before being passed to this constructor: [createScriptURL]. + external factory DomTrustedTypePolicyOptions({ + DomCreateScriptUrlOptionFn? createScriptURL, + }); +} + +/// Type of the function used to configure createScriptURL. +typedef DomCreateScriptUrlOptionFn = String? Function(String input); + +/// A TrustedTypePolicy defines a group of functions which create TrustedType +/// objects. +/// +/// TrustedTypePolicy objects are created by `TrustedTypePolicyFactory.createPolicy`, +/// therefore this class has no constructor. +/// +/// See: https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy +@JS() +@staticInterop +abstract class DomTrustedTypePolicy {} + +/// A subset of TrustedTypePolicy methods. +extension DomTrustedTypePolicyExtension on DomTrustedTypePolicy { + /// Creates a `TrustedScriptURL` for the given [input]. + /// + /// `input` is a string containing the data to be _sanitized_ by the policy. + external DomTrustedScriptURL createScriptURL(String input); +} + +/// Represents a string that a developer can insert into an _injection sink_ +/// that will parse it as an external script. +/// +/// These objects are created via `createScriptURL` and therefore have no +/// constructor. +/// +/// See: https://developer.mozilla.org/en-US/docs/Web/API/TrustedScriptURL +@JS() +@staticInterop +abstract class DomTrustedScriptURL {} + +/// A subset of TrustedScriptURL methods. +extension DomTrustedScriptUrlExtension on DomTrustedScriptURL { + /// Exposes the `toString` JS method of TrustedScriptURL. + String get url => js_util.callMethod(this, 'toString', []); +} + +// The expected set of files that the flutter-engine TrustedType policy is going +// to accept as valid. +const Set _expectedFilesForTT = { + 'canvaskit.js', +}; + +// The definition of the `flutter-engine` TrustedType policy. +// Only accessible if the Trusted Types API is available. +final DomTrustedTypePolicy _ttPolicy = domWindow.trustedTypes!.createPolicy( + 'flutter-engine', + DomTrustedTypePolicyOptions( + // Validates the given [url]. + createScriptURL: allowInterop( + (String url) { + final Uri uri = Uri.parse(url); + if (_expectedFilesForTT.contains(uri.pathSegments.last)) { + return uri.toString(); + } + domWindow.console + .error('URL rejected by TrustedTypes policy flutter-engine: $url' + '(download prevented)'); + + return null; + }, + ), + ), +); + +/// Converts a String `url` into a [DomTrustedScriptURL] object when the +/// Trusted Types API is available, else returns the unmodified `url`. +Object createTrustedScriptUrl(String url) { + if (domWindow.trustedTypes != null) { + // Pass `url` through Flutter Engine's TrustedType policy. + final DomTrustedScriptURL trustedCanvasKitUrl = + _ttPolicy.createScriptURL(url); + + assert(trustedCanvasKitUrl.url != '', + 'URL: $url rejected by TrustedTypePolicy'); + + return trustedCanvasKitUrl; + } + return url; +} + DomMessageChannel createDomMessageChannel() => domCallConstructorString('MessageChannel', [])! as DomMessageChannel; diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_tt_on_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_tt_on_test.dart new file mode 100644 index 0000000000000..05dee0bfa6962 --- /dev/null +++ b/lib/web_ui/test/canvaskit/canvaskit_api_tt_on_test.dart @@ -0,0 +1,61 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; + +import '../matchers.dart'; +import 'canvaskit_api_test.dart'; + +final bool isBlink = browserEngine == BrowserEngine.blink; + +const String goodUrl = 'https://www.unpkg.com/blah-blah/33.x/canvaskit.js'; +const String badUrl = 'https://www.unpkg.com/soemthing/not-canvaskit.js'; + +// These tests need to happen in a separate file, because a Content Security +// Policy cannot be relaxed once set, only made more strict. +void main() { + internalBootstrapBrowserTest(() => testMainWithTTOn); +} + +// Enables Trusted Types, runs all `canvaskit_api_test.dart`, then tests the +// createTrustedScriptUrl function. +void testMainWithTTOn() { + enableTrustedTypes(); + + // Run all standard canvaskit tests, with TT on... + testMain(); + + group('TrustedTypes API supported', () { + test('createTrustedScriptUrl - returns TrustedScriptURL object', () async { + final Object trusted = createTrustedScriptUrl(goodUrl); + + expect(trusted, isA()); + expect((trusted as DomTrustedScriptURL).url, goodUrl); + }); + + test('createTrustedScriptUrl - rejects bad canvaskit.js URL', () async { + expect(() { + createTrustedScriptUrl(badUrl); + }, throwsAssertionError); + }); + }, skip: !isBlink); + + group('Trusted Types API NOT supported', () { + test('createTrustedScriptUrl - returns unmodified url', () async { + expect(createTrustedScriptUrl(badUrl), badUrl); + }); + }, skip: isBlink); +} + +/// Enables Trusted Types by setting the appropriate meta tag in the DOM: +/// +void enableTrustedTypes() { + print('Enabling TrustedTypes in browser window...'); + final DomHTMLMetaElement enableTTMeta = createDomHTMLMetaElement() + ..setAttribute('http-equiv', 'Content-Security-Policy') + ..content = "require-trusted-types-for 'script'"; + domDocument.head!.append(enableTTMeta); +}