Skip to content

Commit

Permalink
feat(core, web): add support for TrustedType (#10312)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Lyokone committed Feb 16, 2023
1 parent edc8e31 commit da74aab
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 18 deletions.
6 changes: 6 additions & 0 deletions docs/setup/_setup_main.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
62 changes: 55 additions & 7 deletions packages/firebase_core/firebase_core/example/web/index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
<head>
<meta charset="UTF-8">
<title>Firebase Core Example</title>
</head>
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
<body>
<script src="main.dart.js" type="application/javascript"></script>
</body>
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF" />

<meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />

<meta
http-equiv="Content-Security-Policy"
content="require-trusted-types-for 'script'"
/>

<title>Firebase Core Example</title>
<link rel="manifest" href="manifest.json" />

<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<script>
document.addEventListener(
'securitypolicyviolation',
console.error.bind(console)
);

window.addEventListener('load', function (ev) {
// Download main.dart.js
_flutter.loader
.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
})
.then(function (engineInitializer) {
return engineInitializer.initializeEngine();
})
.then(function (appRunner) {
return appRunner.runApp();
});
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<void> _injectSrcScript(String src, String windowVar) async {
@visibleForTesting
Future<void> 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 = '''

This comment has been minimized.

Copy link
@eozmen410

eozmen410 Nov 21, 2023

Just a note here, the script.text assignment will still throw a Trusted Types violation as we're assigning a plain string value. We would need to create and assign a TrustedScript to script.text to avoid the TT violation.

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', [
Expand All @@ -132,7 +157,7 @@ class FirebaseCoreWeb extends FirebasePlatform {
return;
}

String version = _firebaseSDKVersion;
String version = firebaseSDKVersion;
List<String> ignored = _ignoredServiceScripts;

await Future.wait(
Expand All @@ -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}',
);
Expand Down
141 changes: 141 additions & 0 deletions packages/firebase_core/firebase_core_web/lib/src/interop/js.dart
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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(<String, String>{
'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<void> done = coreWeb.injectSrcScript(
'https://www.gstatic.com/firebasejs/$version/firebase-app.js',
'firebase_core',
);

expect(done, isA<Future<void>>());
});
});
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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',
),
),
),
);
Expand Down
21 changes: 21 additions & 0 deletions packages/firebase_core/firebase_core_web/test/tools.dart
Original file line number Diff line number Diff line change
@@ -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 `<meta>` tag with the provided [attributes] into the [dom.document].
void injectMetaTag(Map<String, String> attributes) {
final Element meta = document.createElement('meta');
for (final MapEntry<String, String> attribute in attributes.entries) {
js_util.callMethod(
meta,
'setAttribute',
<String>[attribute.key, attribute.value],
);
}
document.head?.append(meta);
}

0 comments on commit da74aab

Please sign in to comment.