Skip to content

Commit da74aab

Browse files
authored
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
1 parent edc8e31 commit da74aab

File tree

8 files changed

+295
-18
lines changed

8 files changed

+295
-18
lines changed

docs/setup/_setup_main.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ flutterfire configure
141141
flutter run
142142
```
143143
144+
#### Using TrustedTypes for web
145+
146+
If you plan to use Firebase on the web, you can use TrustedTypes to prevent
147+
XSS attacks. If TrustedTypes are enabled, Firebase will inject the
148+
scripts into the DOM using TrustedTypes. The policy name are defined as
149+
follows: 'flutterfire-firease_core', 'flutterfire-firebase_auth'... etc.
144150
145151
## **Step 4**: Add Firebase plugins {: #add-plugins}
146152
Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,61 @@
11
<!DOCTYPE html>
22
<html>
3+
<head>
4+
<!--
5+
If you are serving your web app in a path other than the root, change the
6+
href value below to reflect the base path you are serving from.
37
4-
<head>
5-
<meta charset="UTF-8">
6-
<title>Firebase Core Example</title>
7-
</head>
8+
The path provided below has to start and end with a slash "/" in order for
9+
it to work correctly.
810
9-
<body>
10-
<script src="main.dart.js" type="application/javascript"></script>
11-
</body>
11+
For more details:
12+
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
1213
14+
This is a placeholder for base href that will be replaced by the value of
15+
the `--base-href` argument provided to `flutter build`.
16+
-->
17+
<base href="$FLUTTER_BASE_HREF" />
18+
19+
<meta charset="UTF-8" />
20+
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
21+
22+
<meta
23+
http-equiv="Content-Security-Policy"
24+
content="require-trusted-types-for 'script'"
25+
/>
26+
27+
<title>Firebase Core Example</title>
28+
<link rel="manifest" href="manifest.json" />
29+
30+
<script>
31+
// The value below is injected by flutter build, do not touch.
32+
var serviceWorkerVersion = null;
33+
</script>
34+
<!-- This script adds the flutter initialization JS code -->
35+
<script src="flutter.js" defer></script>
36+
</head>
37+
<body>
38+
<script>
39+
document.addEventListener(
40+
'securitypolicyviolation',
41+
console.error.bind(console)
42+
);
43+
44+
window.addEventListener('load', function (ev) {
45+
// Download main.dart.js
46+
_flutter.loader
47+
.loadEntrypoint({
48+
serviceWorker: {
49+
serviceWorkerVersion: serviceWorkerVersion,
50+
},
51+
})
52+
.then(function (engineInitializer) {
53+
return engineInitializer.initializeEngine();
54+
})
55+
.then(function (appRunner) {
56+
return appRunner.runApp();
57+
});
58+
});
59+
</script>
60+
</body>
1361
</html>

packages/firebase_core/firebase_core_web/lib/firebase_core_web.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ import 'dart:html';
1010
import 'dart:js';
1111

1212
import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart';
13+
import 'package:firebase_core_web/src/interop/js.dart';
1314
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
1415
import 'package:js/js_util.dart' as js_util;
16+
import 'package:meta/meta.dart';
17+
1518
import 'src/interop/core.dart' as firebase;
19+
import 'src/interop/js.dart' as js;
1620

1721
part 'src/firebase_app_web.dart';
18-
part 'src/firebase_sdk_version.dart';
1922
part 'src/firebase_core_web.dart';
23+
part 'src/firebase_sdk_version.dart';
2024

2125
/// Returns a [FirebaseAppWeb] instance from [firebase.App].
2226
FirebaseAppPlatform _createFromJsApp(firebase.App jsApp) {

packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ class FirebaseCoreWeb extends FirebasePlatform {
6464
/// You can override the supported version by attaching a version string to
6565
/// the window (window.flutterfire_web_sdk_version = 'x.x.x'). Do so at your
6666
/// own risk as the version might be unsupported or untested against.
67-
String get _firebaseSDKVersion {
67+
@visibleForTesting
68+
String get firebaseSDKVersion {
6869
return context['flutterfire_web_sdk_version'] ??
6970
supportedFirebaseJsSdkVersion;
7071
}
@@ -96,20 +97,44 @@ class FirebaseCoreWeb extends FirebasePlatform {
9697
return [];
9798
}
9899

100+
final String _defaultTrustedPolicyName = 'flutterfire-';
101+
99102
/// Injects a `script` with a `src` dynamically into the head of the current
100103
/// document.
101-
Future<void> _injectSrcScript(String src, String windowVar) async {
104+
@visibleForTesting
105+
Future<void> injectSrcScript(String src, String windowVar) async {
106+
DomTrustedScriptUrl? trustedUrl;
107+
final trustedPolicyName = _defaultTrustedPolicyName + windowVar;
108+
if (trustedTypes != null) {
109+
console.debug(
110+
'TrustedTypes available. Creating policy:',
111+
trustedPolicyName,
112+
);
113+
final DomTrustedTypePolicyFactory factory = trustedTypes!;
114+
try {
115+
final DomTrustedTypePolicy policy = factory.createPolicy(
116+
trustedPolicyName,
117+
DomTrustedTypePolicyOptions(
118+
createScriptURL: allowInterop((String url) => src),
119+
),
120+
);
121+
trustedUrl = policy.createScriptURL(src);
122+
} catch (e) {
123+
rethrow;
124+
}
125+
}
102126
ScriptElement script = ScriptElement();
103127
script.type = 'text/javascript';
104128
script.crossOrigin = 'anonymous';
105129
script.text = '''
106130
window.ff_trigger_$windowVar = async (callback) => {
107-
callback(await import("$src"));
131+
callback(await import("${trustedUrl?.toString() ?? src}"));
108132
};
109133
''';
110134

111135
assert(document.head != null);
112136
document.head!.append(script);
137+
113138
Completer completer = Completer();
114139

115140
context.callMethod('ff_trigger_$windowVar', [
@@ -132,7 +157,7 @@ class FirebaseCoreWeb extends FirebasePlatform {
132157
return;
133158
}
134159

135-
String version = _firebaseSDKVersion;
160+
String version = firebaseSDKVersion;
136161
List<String> ignored = _ignoredServiceScripts;
137162

138163
await Future.wait(
@@ -141,7 +166,7 @@ class FirebaseCoreWeb extends FirebasePlatform {
141166
return Future.value();
142167
}
143168

144-
return _injectSrcScript(
169+
return injectSrcScript(
145170
'https://www.gstatic.com/firebasejs/$version/firebase-${service.name}.js',
146171
'firebase_${service.override ?? service.name}',
147172
);
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/*
6+
// DOM shim. This file contains everything we need from the DOM API written as
7+
// @staticInterop, so we don't need dart:html
8+
// https://developer.mozilla.org/en-US/docs/Web/API/
9+
*/
10+
11+
import 'package:js/js.dart';
12+
13+
/// console interface
14+
@JS()
15+
@staticInterop
16+
@anonymous
17+
abstract class DomConsole {}
18+
19+
/// The interface of window.console
20+
extension DomConsoleExtension on DomConsole {
21+
/// console.debug
22+
external DomConsoleDumpFn get debug;
23+
24+
/// console.info
25+
external DomConsoleDumpFn get info;
26+
27+
/// console.log
28+
external DomConsoleDumpFn get log;
29+
30+
/// console.warn
31+
external DomConsoleDumpFn get warn;
32+
33+
/// console.error
34+
external DomConsoleDumpFn get error;
35+
}
36+
37+
/// Fakey variadic-type for console-dumping methods (like console.log or info).
38+
typedef DomConsoleDumpFn = void Function(
39+
Object? arg, [
40+
Object? arg2,
41+
Object? arg3,
42+
Object? arg4,
43+
Object? arg5,
44+
Object? arg6,
45+
Object? arg7,
46+
Object? arg8,
47+
Object? arg9,
48+
Object? arg10,
49+
]);
50+
51+
/// Error object
52+
@JS('Error')
53+
@staticInterop
54+
abstract class DomError {}
55+
56+
/// Methods on the error object
57+
extension DomErrorExtension on DomError {
58+
/// Error message.
59+
external String? get message;
60+
61+
/// Stack trace.
62+
external String? get stack;
63+
64+
/// Error name. This is determined by the constructor function.
65+
external String get name;
66+
67+
/// Error cause indicating the reason why the current error is thrown.
68+
///
69+
/// This is usually another caught error, or the value provided as the `cause`
70+
/// property of the Error constructor's second argument.
71+
external Object? get cause;
72+
}
73+
74+
/*
75+
// Trusted Types API (TrustedTypePolicy, TrustedScript, TrustedScriptURL)
76+
// https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypesAPI
77+
*/
78+
79+
/// A factory to create `TrustedTypePolicy` objects.
80+
@JS()
81+
@staticInterop
82+
@anonymous
83+
abstract class DomTrustedTypePolicyFactory {}
84+
85+
/// (Some) methods of the [DomTrustedTypePolicyFactory]:
86+
extension DomTrustedTypePolicyFactoryExtension on DomTrustedTypePolicyFactory {
87+
/// createPolicy
88+
external DomTrustedTypePolicy createPolicy(
89+
String policyName,
90+
DomTrustedTypePolicyOptions? policyOptions,
91+
);
92+
}
93+
94+
/// Options to create a trusted type policy.
95+
@JS()
96+
@staticInterop
97+
@anonymous
98+
abstract class DomTrustedTypePolicyOptions {
99+
/// Constructs a TrustedPolicyOptions object in JavaScript.
100+
///
101+
/// The following properties need to be manually wrapped in [allowInterop]
102+
/// before being passed to this constructor: [createScriptURL].
103+
external factory DomTrustedTypePolicyOptions({
104+
DomCreateScriptUrlOptionFn? createScriptURL,
105+
});
106+
}
107+
108+
/// Type of the function to configure createScriptURL
109+
typedef DomCreateScriptUrlOptionFn = String Function(String input);
110+
111+
/// An instance of a TrustedTypePolicy
112+
@JS()
113+
@staticInterop
114+
@anonymous
115+
abstract class DomTrustedTypePolicy {}
116+
117+
/// (Some) methods of the [DomTrustedTypePolicy]
118+
extension DomTrustedTypePolicyExtension on DomTrustedTypePolicy {
119+
/// Create a `TrustedScriptURL` for the given [input].
120+
external DomTrustedScriptUrl createScriptURL(String input);
121+
}
122+
123+
/// An instance of a DomTrustedScriptUrl
124+
@JS()
125+
@staticInterop
126+
@anonymous
127+
abstract class DomTrustedScriptUrl {}
128+
129+
// Getters
130+
131+
/// window.trustedTypes (may or may not be supported by the browser)
132+
@JS()
133+
@staticInterop
134+
@anonymous
135+
external DomTrustedTypePolicyFactory? get trustedTypes;
136+
137+
/// window.console
138+
@JS()
139+
@staticInterop
140+
@anonymous
141+
external DomConsole get console;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@TestOn('browser')
6+
import 'package:firebase_core_web/firebase_core_web.dart';
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
import 'tools.dart';
10+
11+
// NOTE: This file needs to be separated from the others because Content
12+
// Security Policies can never be *relaxed* once set.
13+
14+
void main() {
15+
group('injectScript (TrustedTypes configured)', () {
16+
injectMetaTag(<String, String>{
17+
'http-equiv': 'Content-Security-Policy',
18+
'content': "trusted-types flutterfire-firebase_core 'allow-duplicates';",
19+
});
20+
21+
test('Should inject Firebase Core script properly', () {
22+
final coreWeb = FirebaseCoreWeb();
23+
final version = coreWeb.firebaseSDKVersion;
24+
final Future<void> done = coreWeb.injectSrcScript(
25+
'https://www.gstatic.com/firebasejs/$version/firebase-app.js',
26+
'firebase_core',
27+
);
28+
29+
expect(done, isA<Future<void>>());
30+
});
31+
});
32+
}

packages/firebase_core/firebase_core_web/test/firebase_core_web_test.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// ignore_for_file: require_trailing_commas
21
// Copyright 2020 The Chromium Authors. All rights reserved.
32
// Use of this source code is governed by a BSD-style license that can be
43
// found in the LICENSE file.
@@ -23,10 +22,11 @@ void main() {
2322
(String name) => FirebaseAppMock(
2423
name: name,
2524
options: FirebaseAppOptionsMock(
26-
apiKey: 'abc',
27-
appId: '123',
28-
messagingSenderId: 'msg',
29-
projectId: 'test'),
25+
apiKey: 'abc',
26+
appId: '123',
27+
messagingSenderId: 'msg',
28+
projectId: 'test',
29+
),
3030
),
3131
),
3232
);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2020 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:html';
6+
7+
import 'package:firebase_core_web/src/interop/js.dart' as dom;
8+
import 'package:js/js_util.dart' as js_util;
9+
10+
/// Injects a `<meta>` tag with the provided [attributes] into the [dom.document].
11+
void injectMetaTag(Map<String, String> attributes) {
12+
final Element meta = document.createElement('meta');
13+
for (final MapEntry<String, String> attribute in attributes.entries) {
14+
js_util.callMethod(
15+
meta,
16+
'setAttribute',
17+
<String>[attribute.key, attribute.value],
18+
);
19+
}
20+
document.head?.append(meta);
21+
}

0 commit comments

Comments
 (0)