Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fail screenshot/replay creation when a potentially sensitive widget is not masked #2375

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions flutter/lib/src/sentry_replay_options.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
Expand Down Expand Up @@ -85,6 +86,30 @@
rules.add(const SentryMaskingConstantRule<EditableText>(
SentryMaskingDecision.mask));
}

// In Debug mode, check if users explicitly masks (or unmasks) widgets that
// look like they should be masked, e.g. Videos, WebViews, etc.
if (kDebugMode) {
rules.add(
SentryMaskingCustomRule<Widget>((Element element, Widget widget) {
final type = widget.runtimeType.toString();
final regexp = 'video|webview|password|pinput|camera|chart';
if (RegExp(regexp, caseSensitive: false).hasMatch(type)) {
final optionsName = 'options.experimental.replay';
throw Exception(

Check warning on line 99 in flutter/lib/src/sentry_replay_options.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/sentry_replay_options.dart#L99

Added line #L99 was not covered by tests
'Widget "$widget" name matches widgets that should usually be '
'masked because they may contain sensitive data. Because this '
'widget comes from a third-party plugin or your code, Sentry '
'cannot reliably mask it in release builds (due to obfuscation).'
'Please mask it explicitly using $optionsName.mask<$type>(). '
'If you want to silence this exception and keep the widget '
'visible in captures, you can use $optionsName.unmask<$type>(). '
'Note: the RegExp matched is: $regexp (case insensitive).');
}
return SentryMaskingDecision.continueProcessing;
}));
}

return SentryMaskingConfig(rules);
}

Expand Down
20 changes: 13 additions & 7 deletions flutter/test/replay/masking_config_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,8 @@ void main() async {
.map((rule) => rule.toString())
// These normalize the string on VM & js & wasm:
.map((str) => str.replaceAll(
RegExp(
r"SentryMaskingDecision from:? [fF]unction '?_maskImagesExceptAssets[@(].*",
dotAll: true),
'SentryMaskingDecision)'))
RegExp(r"=> SentryMaskingDecision from:? .*", dotAll: true),
'=> SentryMaskingDecision)'))
.map((str) => str.replaceAll(
' from: (element, widget) => masking_config.SentryMaskingDecision.mask',
''))
Expand All @@ -136,7 +134,8 @@ void main() async {
...alwaysEnabledRules,
'$SentryMaskingCustomRule<$Image>(Closure: (Element, Widget) => SentryMaskingDecision)',
'$SentryMaskingConstantRule<$Text>(mask)',
'$SentryMaskingConstantRule<$EditableText>(mask)'
'$SentryMaskingConstantRule<$EditableText>(mask)',
'$SentryMaskingCustomRule<$Widget>(Closure: ($Element, $Widget) => $SentryMaskingDecision)'
]);
});

Expand All @@ -148,6 +147,7 @@ void main() async {
expect(rulesAsStrings(sut), [
...alwaysEnabledRules,
'$SentryMaskingConstantRule<$Image>(mask)',
'$SentryMaskingCustomRule<$Widget>(Closure: ($Element, $Widget) => $SentryMaskingDecision)'
]);
});

Expand All @@ -159,6 +159,7 @@ void main() async {
expect(rulesAsStrings(sut), [
...alwaysEnabledRules,
'$SentryMaskingCustomRule<$Image>(Closure: (Element, Widget) => SentryMaskingDecision)',
'$SentryMaskingCustomRule<$Widget>(Closure: ($Element, $Widget) => $SentryMaskingDecision)'
]);
});

Expand All @@ -171,6 +172,7 @@ void main() async {
...alwaysEnabledRules,
'$SentryMaskingConstantRule<$Text>(mask)',
'$SentryMaskingConstantRule<$EditableText>(mask)',
'$SentryMaskingCustomRule<$Widget>(Closure: ($Element, $Widget) => $SentryMaskingDecision)'
]);
});

Expand All @@ -179,15 +181,19 @@ void main() async {
..maskAllText = false
..maskAllImages = false
..maskAssetImages = false;
expect(rulesAsStrings(sut), alwaysEnabledRules);
expect(rulesAsStrings(sut), [
...alwaysEnabledRules,
'$SentryMaskingCustomRule<$Widget>(Closure: ($Element, $Widget) => $SentryMaskingDecision)'
]);
});

group('user rules', () {
final defaultRules = [
...alwaysEnabledRules,
'$SentryMaskingCustomRule<$Image>(Closure: (Element, Widget) => SentryMaskingDecision)',
'$SentryMaskingConstantRule<$Text>(mask)',
'$SentryMaskingConstantRule<$EditableText>(mask)'
'$SentryMaskingConstantRule<$EditableText>(mask)',
'$SentryMaskingCustomRule<$Widget>(Closure: ($Element, $Widget) => $SentryMaskingDecision)'
];
test('mask() takes precedence', () {
final sut = SentryReplayOptions();
Expand Down
1 change: 1 addition & 0 deletions flutter/test/replay/test_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Future<Element> pumpTestElement(WidgetTester tester,
Offstage(offstage: true, child: newImage()),
Text(dummyText),
Material(child: TextFormField()),
Material(child: TextField()),
SizedBox(
width: 100,
height: 20,
Expand Down
7 changes: 4 additions & 3 deletions flutter/test/replay/widget_filter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ void main() async {
final sut = createSut(redactText: true);
final element = await pumpTestElement(tester);
sut.obscure(element, 1.0, defaultBounds);
expect(sut.items.length, 5);
expect(sut.items.length, 6);
});

testWidgets('does not redact text when disabled', (tester) async {
Expand All @@ -53,12 +53,13 @@ void main() async {
final sut = createSut(redactText: true);
final element = await pumpTestElement(tester);
sut.obscure(element, 1.0, defaultBounds);
expect(sut.items.length, 5);
expect(sut.items.length, 6);
expect(boundsRect(sut.items[0]), '624x48');
expect(boundsRect(sut.items[1]), '169x20');
expect(boundsRect(sut.items[2]), '800x192');
expect(boundsRect(sut.items[3]), '800x24');
expect(boundsRect(sut.items[4]), '50x20');
expect(boundsRect(sut.items[4]), '800x24');
expect(boundsRect(sut.items[5]), '50x20');
});
});

Expand Down
Loading