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);
+}