From 907f3107004a19813a96ca8223d68cbb30e48e24 Mon Sep 17 00:00:00 2001 From: Diego Rabatone Oliveira Date: Thu, 16 Apr 2026 14:31:11 -0300 Subject: [PATCH] security: disable JavaScript in WebViews loading static content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disable JavaScript execution in all 3 WebView pages that load static server-rendered content (privacy policy, terms of use, FAQ). None of these pages use JavaScript — the mailto: handler in AboutPenhasPage works via NavigationDelegate at the Dart level, not via JS. Enabling JavaScript unnecessarily creates an XSS attack vector: if the server is compromised or traffic is intercepted (especially relevant given the absence of certificate pinning), malicious JavaScript could execute in the WebView context. Changes: - privacy_policy_page.dart: JavaScriptMode.unrestricted -> disabled - terms_of_use_page.dart: JavaScriptMode.unrestricted -> disabled - about_penhas_page.dart: JavaScriptMode.unrestricted -> disabled - webview_mocks.dart: FakeWebViewPlatform now exposes lastController; FakeWebViewController captures lastJavaScriptMode for assertions; added setPlatformNavigationDelegate override for AboutPenhasPage - Added testWidgets asserting JavaScriptMode.disabled in all 3 pages - Created about_penhas_page_test.dart (previously had no test coverage) Before: All WebViews used JavaScriptMode.unrestricted. After: All WebViews use JavaScriptMode.disabled. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../privacy_policy/privacy_policy_page.dart | 2 +- .../terms_of_use/terms_of_use_page.dart | 2 +- .../presentation/pages/about_penhas_page.dart | 2 +- .../presentation/mocks/webview_mocks.dart | 18 ++++++- .../privacy_policy_page_test.dart | 23 +++++++++ .../terms_of_use/terms_of_use_page_test.dart | 23 +++++++++ .../pages/about_penhas_page_test.dart | 50 +++++++++++++++++++ 7 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 test/app/features/main_menu/presentation/pages/about_penhas_page_test.dart diff --git a/lib/app/features/authentication/presentation/sign_in/privacy_policy/privacy_policy_page.dart b/lib/app/features/authentication/presentation/sign_in/privacy_policy/privacy_policy_page.dart index 17259e092..80e3c485d 100644 --- a/lib/app/features/authentication/presentation/sign_in/privacy_policy/privacy_policy_page.dart +++ b/lib/app/features/authentication/presentation/sign_in/privacy_policy/privacy_policy_page.dart @@ -22,7 +22,7 @@ class PrivacyPolicyPage extends StatelessWidget { ), body: WebViewWidget( controller: WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setJavaScriptMode(JavaScriptMode.disabled) ..loadRequest(baseUrl.resolve('web/politica-privacidade')), ), ); diff --git a/lib/app/features/authentication/presentation/sign_in/terms_of_use/terms_of_use_page.dart b/lib/app/features/authentication/presentation/sign_in/terms_of_use/terms_of_use_page.dart index 693dd1862..ed3d13f24 100644 --- a/lib/app/features/authentication/presentation/sign_in/terms_of_use/terms_of_use_page.dart +++ b/lib/app/features/authentication/presentation/sign_in/terms_of_use/terms_of_use_page.dart @@ -22,7 +22,7 @@ class TermsOfUsePage extends StatelessWidget { ), body: WebViewWidget( controller: WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setJavaScriptMode(JavaScriptMode.disabled) ..loadRequest(baseUrl.resolve('web/termos-de-uso')), ), ); diff --git a/lib/app/features/main_menu/presentation/pages/about_penhas_page.dart b/lib/app/features/main_menu/presentation/pages/about_penhas_page.dart index c640a9940..7201ea71b 100644 --- a/lib/app/features/main_menu/presentation/pages/about_penhas_page.dart +++ b/lib/app/features/main_menu/presentation/pages/about_penhas_page.dart @@ -23,7 +23,7 @@ class AboutPenhasPage extends StatelessWidget { ), body: WebViewWidget( controller: WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setJavaScriptMode(JavaScriptMode.disabled) ..setNavigationDelegate(NavigationDelegate( onNavigationRequest: (request) { if (request.url.startsWith('mailto')) { diff --git a/test/app/features/authentication/presentation/mocks/webview_mocks.dart b/test/app/features/authentication/presentation/mocks/webview_mocks.dart index ef37d526b..d3cea26f6 100644 --- a/test/app/features/authentication/presentation/mocks/webview_mocks.dart +++ b/test/app/features/authentication/presentation/mocks/webview_mocks.dart @@ -3,10 +3,15 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte /// Fake implementation of WebViewPlatform for testing. class FakeWebViewPlatform extends WebViewPlatform { + /// The last controller created by this platform, useful for asserting + /// configuration values (e.g. JavaScriptMode) set during widget build. + FakeWebViewController? lastController; + @override PlatformWebViewController createPlatformWebViewController( PlatformWebViewControllerCreationParams params) { - return FakeWebViewController(params); + lastController = FakeWebViewController(params); + return lastController!; } @override @@ -21,6 +26,9 @@ class FakeWebViewController extends PlatformWebViewController { FakeWebViewController(PlatformWebViewControllerCreationParams params) : super.implementation(params); + /// Captures the last JavaScriptMode set, enabling test assertions. + JavaScriptMode? lastJavaScriptMode; + @override Future loadRequest(LoadRequestParams params) async { // Mock the behavior when loadRequest is called. @@ -31,7 +39,7 @@ class FakeWebViewController extends PlatformWebViewController { @override Future setJavaScriptMode(JavaScriptMode javaScriptMode) async { - // Simulate setting JavaScript mode. + lastJavaScriptMode = javaScriptMode; } @override @@ -43,6 +51,12 @@ class FakeWebViewController extends PlatformWebViewController { Future enableZoom(bool enabled) async { // Simulate enabling/disabling zoom. } + + @override + Future setPlatformNavigationDelegate( + PlatformNavigationDelegate handler) async { + // Simulate setting navigation delegate (needed by AboutPenhasPage). + } } /// Fake WebView Widget diff --git a/test/app/features/authentication/presentation/sign_in/privacy_policy/privacy_policy_page_test.dart b/test/app/features/authentication/presentation/sign_in/privacy_policy/privacy_policy_page_test.dart index 729b0f9f4..3089a350d 100644 --- a/test/app/features/authentication/presentation/sign_in/privacy_policy/privacy_policy_page_test.dart +++ b/test/app/features/authentication/presentation/sign_in/privacy_policy/privacy_policy_page_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:penhas/app/features/authentication/presentation/sign_in/privacy_policy/privacy_policy_page.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -24,5 +25,27 @@ void main() { ), ); }); + + testWidgets('should configure WebView with JavaScript disabled', + (tester) async { + final fakePlatform = FakeWebViewPlatform(); + WebViewPlatform.instance = fakePlatform; + + await tester.pumpWidget( + MaterialApp( + home: PrivacyPolicyPage( + baseUrl: Uri( + scheme: 'https', + host: 'www.example.com', + ), + ), + ), + ); + + expect( + fakePlatform.lastController?.lastJavaScriptMode, + equals(JavaScriptMode.disabled), + ); + }); }); } diff --git a/test/app/features/authentication/presentation/sign_in/terms_of_use/terms_of_use_page_test.dart b/test/app/features/authentication/presentation/sign_in/terms_of_use/terms_of_use_page_test.dart index c51d16fe4..15561c25f 100644 --- a/test/app/features/authentication/presentation/sign_in/terms_of_use/terms_of_use_page_test.dart +++ b/test/app/features/authentication/presentation/sign_in/terms_of_use/terms_of_use_page_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:penhas/app/features/authentication/presentation/sign_in/terms_of_use/terms_of_use_page.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -24,5 +25,27 @@ void main() { ), ); }); + + testWidgets('should configure WebView with JavaScript disabled', + (tester) async { + final fakePlatform = FakeWebViewPlatform(); + WebViewPlatform.instance = fakePlatform; + + await tester.pumpWidget( + MaterialApp( + home: TermsOfUsePage( + baseUrl: Uri( + scheme: 'https', + host: 'www.example.com', + ), + ), + ), + ); + + expect( + fakePlatform.lastController?.lastJavaScriptMode, + equals(JavaScriptMode.disabled), + ); + }); }); } diff --git a/test/app/features/main_menu/presentation/pages/about_penhas_page_test.dart b/test/app/features/main_menu/presentation/pages/about_penhas_page_test.dart new file mode 100644 index 000000000..7a21ffe76 --- /dev/null +++ b/test/app/features/main_menu/presentation/pages/about_penhas_page_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:penhas/app/features/main_menu/presentation/pages/about_penhas_page.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import '../../../../../utils/golden_tests.dart'; +import '../../../../features/authentication/presentation/mocks/webview_mocks.dart'; + +void main() { + setUp(() { + WebViewPlatform.instance = FakeWebViewPlatform(); + }); + + group(AboutPenhasPage, () { + group('golden test', () { + screenshotTest( + 'looks as expected', + fileName: 'about_penhas_page', + pageBuilder: () => AboutPenhasPage( + baseUrl: Uri( + scheme: 'https', + host: 'www.example.com', + ), + ), + ); + }); + + testWidgets('should configure WebView with JavaScript disabled', + (tester) async { + final fakePlatform = FakeWebViewPlatform(); + WebViewPlatform.instance = fakePlatform; + + await tester.pumpWidget( + MaterialApp( + home: AboutPenhasPage( + baseUrl: Uri( + scheme: 'https', + host: 'www.example.com', + ), + ), + ), + ); + + expect( + fakePlatform.lastController?.lastJavaScriptMode, + equals(JavaScriptMode.disabled), + ); + }); + }); +}