Skip to content

Commit 44e0d32

Browse files
cbrackenpull[bot]
authored andcommitted
[macOS] support secure restorable state by default (flutter#151605)
By default, Flutter apps only do default AppKit app serialisation of Window location etc. and by default, state serialisation in AppKit apps is compatible with `NSSecureCoding`. AppKit apps generated since Xcode 13.2 include this method in the app delegate generated by the default app template. Background ========== This method was added to opt into having [de]serialization require a coder implementing the `NSSecureCoding` protocol. Apple wasn't able to force this across the board, because `NSSecureCoding` limits certain behaviours during deserialisation, which some third-party apps have have previously relied on. Specific background on the sorts of vulnerabilities that `NSSecureCoding` was designed to prevent are described in the `NSSecureCoding` documentation: https://developer.apple.com/documentation/foundation/nssecurecoding?language=objc A demonstration of a root privilege escalation and SIP bypass vulnerability is described in the following blog post: https://sector7.computest.nl/post/2022-08-process-injection-breaking-all-macos-security-layers-with-a-single-vulnerability/ Fixes: flutter#150062 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 17a1200 commit 44e0d32

File tree

19 files changed

+309
-0
lines changed

19 files changed

+309
-0
lines changed

dev/a11y_assessments/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

dev/benchmarks/complex_layout/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

dev/benchmarks/macrobenchmarks/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

dev/integration_tests/channels/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

dev/integration_tests/flavors/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

dev/integration_tests/flutter_gallery/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

dev/integration_tests/ui/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

dev/manual_tests/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

examples/api/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

examples/flutter_view/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

examples/hello_world/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

examples/image_list/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

examples/layers/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

examples/platform_channel/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

examples/platform_view/macos/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
1010
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
1111
return true
1212
}
13+
14+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
15+
return true
16+
}
1317
}

packages/flutter_tools/lib/src/macos/build_macos.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import 'migrations/flutter_application_migration.dart';
2727
import 'migrations/macos_deployment_target_migration.dart';
2828
import 'migrations/nsapplicationmain_deprecation_migration.dart';
2929
import 'migrations/remove_macos_framework_link_and_embedding_migration.dart';
30+
import 'migrations/secure_restorable_state_migration.dart';
3031

3132
/// When run in -quiet mode, Xcode should only print from the underlying tasks to stdout.
3233
/// Passing this regexp to trace moves the stdout output to stderr.
@@ -87,6 +88,7 @@ Future<void> buildMacOS({
8788
XcodeThinBinaryBuildPhaseInputPathsMigration(flutterProject.macos, globals.logger),
8889
FlutterApplicationMigration(flutterProject.macos, globals.logger),
8990
NSApplicationMainDeprecationMigration(flutterProject.macos, globals.logger),
91+
SecureRestorableStateMigration(flutterProject.macos, globals.logger),
9092
if (flutterProject.usesSwiftPackageManager && flutterProject.macos.flutterPluginSwiftPackageManifest.existsSync())
9193
SwiftPackageManagerIntegrationMigration(
9294
flutterProject.macos,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2014 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+
import '../../base/file_system.dart';
6+
import '../../base/project_migrator.dart';
7+
import '../../xcode_project.dart';
8+
9+
const String _appDelegateFileBefore = r'''
10+
class AppDelegate: FlutterAppDelegate {
11+
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
12+
return true
13+
}
14+
}''';
15+
16+
const String _appDelegateFileAfter = r'''
17+
class AppDelegate: FlutterAppDelegate {
18+
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
19+
return true
20+
}
21+
22+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
23+
return true
24+
}
25+
}''';
26+
27+
/// Add `applicationSupportsSecureRestorableState` if not already present.
28+
///
29+
/// In all new AppKit apps since Xcode 13.2, the AppDelegate template includes
30+
/// this method, which opts in to requiring safe deserialization via the
31+
/// `NSSecureCoding` protocol. Because this required new API, existing apps
32+
/// need to opt in to this behavior.
33+
///
34+
/// Since nearly all Flutter macOS apps will be doing serialization of Flutter
35+
/// state via Dart code, it's a very safe bet that the vast majority of
36+
/// existing Flutter apps can safely enable this flag. The few apps that
37+
/// are doing serialization via older insecure APIs can update the migrated
38+
/// code to return false.
39+
///
40+
/// See:
41+
/// https://developer.apple.com/documentation/foundation/nssecurecoding?language=objc
42+
class SecureRestorableStateMigration extends ProjectMigrator {
43+
SecureRestorableStateMigration(
44+
MacOSProject project,
45+
super.logger,
46+
) : _appDelegateSwift = project.appDelegateSwift;
47+
48+
final File _appDelegateSwift;
49+
50+
@override
51+
Future<void> migrate() async {
52+
// Skip this migration if the project uses Objective-C.
53+
if (!_appDelegateSwift.existsSync()) {
54+
logger.printTrace(
55+
'macos/Runner/AppDelegate.swift not found. Skipping applicationSupportsSecureRestorableState migration.'
56+
);
57+
return;
58+
}
59+
final String original = _appDelegateSwift.readAsStringSync();
60+
61+
// If we have an AppDelegate.swift, but can't migrate, log a warning.
62+
if (!original.contains(_appDelegateFileBefore)) {
63+
if (original.contains('applicationSupportsSecureRestorableState')) {
64+
// User has already overridden this method. Exit quietly.
65+
return;
66+
}
67+
68+
logger.printWarning('''
69+
macos/Runner/AppDelegate.swift has been modified and cannot be automatically migrated.
70+
We recommend developers override applicationSupportsSecureRestorableState in AppDelegate.swift as follows:
71+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
72+
return true
73+
}
74+
''');
75+
}
76+
77+
// Migrate the macos/Runner/AppDelegate.swift file.
78+
final String migrated = original.replaceFirst(_appDelegateFileBefore, _appDelegateFileAfter);
79+
if (original == migrated) {
80+
return;
81+
}
82+
83+
logger.printWarning(
84+
'macos/Runner/AppDelegate.swift does not override applicationSupportsSecureRestorableState. Updating.'
85+
);
86+
_appDelegateSwift.writeAsStringSync(migrated);
87+
}
88+
}

packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/AppDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate {
66
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
77
return true
88
}
9+
10+
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
11+
return true
12+
}
913
}

0 commit comments

Comments
 (0)