From da74aabb0aa7350319179c1cb586b7bd3591d415 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Thu, 16 Feb 2023 13:39:07 +0100 Subject: [PATCH] feat(core, web): add support for TrustedType (#10312) * feat(core): support TrustedType * feat(core): support TrustedType * feat(core): support TrustedType * feat(core): add trusted types support * feat(core): add trusted types support * feat(core): add trusted types support * feat(core): add trusted types support * feat(core): add trusted types support * feat(core): add trusted types support * feat(core): add trusted types support * feat(core): add documentation --- docs/setup/_setup_main.md | 6 + .../firebase_core/example/web/index.html | 62 +++++++- .../lib/firebase_core_web.dart | 6 +- .../lib/src/firebase_core_web.dart | 35 ++++- .../firebase_core_web/lib/src/interop/js.dart | 141 ++++++++++++++++++ .../test/firebase_core_tt_test.dart | 32 ++++ .../test/firebase_core_web_test.dart | 10 +- .../firebase_core_web/test/tools.dart | 21 +++ 8 files changed, 295 insertions(+), 18 deletions(-) create mode 100644 packages/firebase_core/firebase_core_web/lib/src/interop/js.dart create mode 100644 packages/firebase_core/firebase_core_web/test/firebase_core_tt_test.dart create mode 100644 packages/firebase_core/firebase_core_web/test/tools.dart diff --git a/docs/setup/_setup_main.md b/docs/setup/_setup_main.md index 272d1cd37d2f..04ed75e7aba1 100644 --- a/docs/setup/_setup_main.md +++ b/docs/setup/_setup_main.md @@ -141,6 +141,12 @@ flutterfire configure flutter run ``` +#### Using TrustedTypes for web + +If you plan to use Firebase on the web, you can use TrustedTypes to prevent +XSS attacks. If TrustedTypes are enabled, Firebase will inject the +scripts into the DOM using TrustedTypes. The policy name are defined as +follows: 'flutterfire-firease_core', 'flutterfire-firebase_auth'... etc. ## **Step 4**: Add Firebase plugins {: #add-plugins} diff --git a/packages/firebase_core/firebase_core/example/web/index.html b/packages/firebase_core/firebase_core/example/web/index.html index 1fa3be6941df..091baec82002 100644 --- a/packages/firebase_core/firebase_core/example/web/index.html +++ b/packages/firebase_core/firebase_core/example/web/index.html @@ -1,13 +1,61 @@ + + + + + + + + + + Firebase Core Example + + + + + + + + + diff --git a/packages/firebase_core/firebase_core_web/lib/firebase_core_web.dart b/packages/firebase_core/firebase_core_web/lib/firebase_core_web.dart index a2ae0d1bc33d..fb986285b4d2 100644 --- a/packages/firebase_core/firebase_core_web/lib/firebase_core_web.dart +++ b/packages/firebase_core/firebase_core_web/lib/firebase_core_web.dart @@ -10,13 +10,17 @@ import 'dart:html'; import 'dart:js'; import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; +import 'package:firebase_core_web/src/interop/js.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:js/js_util.dart' as js_util; +import 'package:meta/meta.dart'; + import 'src/interop/core.dart' as firebase; +import 'src/interop/js.dart' as js; part 'src/firebase_app_web.dart'; -part 'src/firebase_sdk_version.dart'; part 'src/firebase_core_web.dart'; +part 'src/firebase_sdk_version.dart'; /// Returns a [FirebaseAppWeb] instance from [firebase.App]. FirebaseAppPlatform _createFromJsApp(firebase.App jsApp) { diff --git a/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart b/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart index 86043dc396c3..6073747b4d07 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart @@ -64,7 +64,8 @@ class FirebaseCoreWeb extends FirebasePlatform { /// You can override the supported version by attaching a version string to /// the window (window.flutterfire_web_sdk_version = 'x.x.x'). Do so at your /// own risk as the version might be unsupported or untested against. - String get _firebaseSDKVersion { + @visibleForTesting + String get firebaseSDKVersion { return context['flutterfire_web_sdk_version'] ?? supportedFirebaseJsSdkVersion; } @@ -96,20 +97,44 @@ class FirebaseCoreWeb extends FirebasePlatform { return []; } + final String _defaultTrustedPolicyName = 'flutterfire-'; + /// Injects a `script` with a `src` dynamically into the head of the current /// document. - Future _injectSrcScript(String src, String windowVar) async { + @visibleForTesting + Future injectSrcScript(String src, String windowVar) async { + DomTrustedScriptUrl? trustedUrl; + final trustedPolicyName = _defaultTrustedPolicyName + windowVar; + if (trustedTypes != null) { + console.debug( + 'TrustedTypes available. Creating policy:', + trustedPolicyName, + ); + final DomTrustedTypePolicyFactory factory = trustedTypes!; + try { + final DomTrustedTypePolicy policy = factory.createPolicy( + trustedPolicyName, + DomTrustedTypePolicyOptions( + createScriptURL: allowInterop((String url) => src), + ), + ); + trustedUrl = policy.createScriptURL(src); + } catch (e) { + rethrow; + } + } ScriptElement script = ScriptElement(); script.type = 'text/javascript'; script.crossOrigin = 'anonymous'; script.text = ''' window.ff_trigger_$windowVar = async (callback) => { - callback(await import("$src")); + callback(await import("${trustedUrl?.toString() ?? src}")); }; '''; assert(document.head != null); document.head!.append(script); + Completer completer = Completer(); context.callMethod('ff_trigger_$windowVar', [ @@ -132,7 +157,7 @@ class FirebaseCoreWeb extends FirebasePlatform { return; } - String version = _firebaseSDKVersion; + String version = firebaseSDKVersion; List ignored = _ignoredServiceScripts; await Future.wait( @@ -141,7 +166,7 @@ class FirebaseCoreWeb extends FirebasePlatform { return Future.value(); } - return _injectSrcScript( + return injectSrcScript( 'https://www.gstatic.com/firebasejs/$version/firebase-${service.name}.js', 'firebase_${service.override ?? service.name}', ); diff --git a/packages/firebase_core/firebase_core_web/lib/src/interop/js.dart b/packages/firebase_core/firebase_core_web/lib/src/interop/js.dart new file mode 100644 index 000000000000..0a720243449d --- /dev/null +++ b/packages/firebase_core/firebase_core_web/lib/src/interop/js.dart @@ -0,0 +1,141 @@ +// 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. + +/* +// DOM shim. This file contains everything we need from the DOM API written as +// @staticInterop, so we don't need dart:html +// https://developer.mozilla.org/en-US/docs/Web/API/ +*/ + +import 'package:js/js.dart'; + +/// console interface +@JS() +@staticInterop +@anonymous +abstract class DomConsole {} + +/// The interface of window.console +extension DomConsoleExtension on DomConsole { + /// console.debug + external DomConsoleDumpFn get debug; + + /// console.info + external DomConsoleDumpFn get info; + + /// console.log + external DomConsoleDumpFn get log; + + /// console.warn + external DomConsoleDumpFn get warn; + + /// console.error + external DomConsoleDumpFn get error; +} + +/// Fakey variadic-type for console-dumping methods (like console.log or info). +typedef DomConsoleDumpFn = void Function( + Object? arg, [ + Object? arg2, + Object? arg3, + Object? arg4, + Object? arg5, + Object? arg6, + Object? arg7, + Object? arg8, + Object? arg9, + Object? arg10, +]); + +/// Error object +@JS('Error') +@staticInterop +abstract class DomError {} + +/// Methods on the error object +extension DomErrorExtension on DomError { + /// Error message. + external String? get message; + + /// Stack trace. + external String? get stack; + + /// Error name. This is determined by the constructor function. + external String get name; + + /// Error cause indicating the reason why the current error is thrown. + /// + /// This is usually another caught error, or the value provided as the `cause` + /// property of the Error constructor's second argument. + external Object? get cause; +} + +/* +// Trusted Types API (TrustedTypePolicy, TrustedScript, TrustedScriptURL) +// https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypesAPI +*/ + +/// A factory to create `TrustedTypePolicy` objects. +@JS() +@staticInterop +@anonymous +abstract class DomTrustedTypePolicyFactory {} + +/// (Some) methods of the [DomTrustedTypePolicyFactory]: +extension DomTrustedTypePolicyFactoryExtension on DomTrustedTypePolicyFactory { + /// createPolicy + external DomTrustedTypePolicy createPolicy( + String policyName, + DomTrustedTypePolicyOptions? policyOptions, + ); +} + +/// Options to create a trusted type policy. +@JS() +@staticInterop +@anonymous +abstract class DomTrustedTypePolicyOptions { + /// Constructs a TrustedPolicyOptions object in JavaScript. + /// + /// 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 to configure createScriptURL +typedef DomCreateScriptUrlOptionFn = String Function(String input); + +/// An instance of a TrustedTypePolicy +@JS() +@staticInterop +@anonymous +abstract class DomTrustedTypePolicy {} + +/// (Some) methods of the [DomTrustedTypePolicy] +extension DomTrustedTypePolicyExtension on DomTrustedTypePolicy { + /// Create a `TrustedScriptURL` for the given [input]. + external DomTrustedScriptUrl createScriptURL(String input); +} + +/// An instance of a DomTrustedScriptUrl +@JS() +@staticInterop +@anonymous +abstract class DomTrustedScriptUrl {} + +// Getters + +/// window.trustedTypes (may or may not be supported by the browser) +@JS() +@staticInterop +@anonymous +external DomTrustedTypePolicyFactory? get trustedTypes; + +/// window.console +@JS() +@staticInterop +@anonymous +external DomConsole get console; diff --git a/packages/firebase_core/firebase_core_web/test/firebase_core_tt_test.dart b/packages/firebase_core/firebase_core_web/test/firebase_core_tt_test.dart new file mode 100644 index 000000000000..5a0e8d22cb41 --- /dev/null +++ b/packages/firebase_core/firebase_core_web/test/firebase_core_tt_test.dart @@ -0,0 +1,32 @@ +// 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. + +@TestOn('browser') +import 'package:firebase_core_web/firebase_core_web.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'tools.dart'; + +// NOTE: This file needs to be separated from the others because Content +// Security Policies can never be *relaxed* once set. + +void main() { + group('injectScript (TrustedTypes configured)', () { + injectMetaTag({ + 'http-equiv': 'Content-Security-Policy', + 'content': "trusted-types flutterfire-firebase_core 'allow-duplicates';", + }); + + test('Should inject Firebase Core script properly', () { + final coreWeb = FirebaseCoreWeb(); + final version = coreWeb.firebaseSDKVersion; + final Future done = coreWeb.injectSrcScript( + 'https://www.gstatic.com/firebasejs/$version/firebase-app.js', + 'firebase_core', + ); + + expect(done, isA>()); + }); + }); +} diff --git a/packages/firebase_core/firebase_core_web/test/firebase_core_web_test.dart b/packages/firebase_core/firebase_core_web/test/firebase_core_web_test.dart index a738cc24127a..974b883e2a5f 100644 --- a/packages/firebase_core/firebase_core_web/test/firebase_core_web_test.dart +++ b/packages/firebase_core/firebase_core_web/test/firebase_core_web_test.dart @@ -1,4 +1,3 @@ -// ignore_for_file: require_trailing_commas // Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -23,10 +22,11 @@ void main() { (String name) => FirebaseAppMock( name: name, options: FirebaseAppOptionsMock( - apiKey: 'abc', - appId: '123', - messagingSenderId: 'msg', - projectId: 'test'), + apiKey: 'abc', + appId: '123', + messagingSenderId: 'msg', + projectId: 'test', + ), ), ), ); diff --git a/packages/firebase_core/firebase_core_web/test/tools.dart b/packages/firebase_core/firebase_core_web/test/tools.dart new file mode 100644 index 000000000000..c6982ff84443 --- /dev/null +++ b/packages/firebase_core/firebase_core_web/test/tools.dart @@ -0,0 +1,21 @@ +// Copyright 2020 The Chromium 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 'dart:html'; + +import 'package:firebase_core_web/src/interop/js.dart' as dom; +import 'package:js/js_util.dart' as js_util; + +/// Injects a `` tag with the provided [attributes] into the [dom.document]. +void injectMetaTag(Map attributes) { + final Element meta = document.createElement('meta'); + for (final MapEntry attribute in attributes.entries) { + js_util.callMethod( + meta, + 'setAttribute', + [attribute.key, attribute.value], + ); + } + document.head?.append(meta); +}