diff --git a/ios/web/js_messaging/java_script_feature_inttest.mm b/ios/web/js_messaging/java_script_feature_inttest.mm index b533a1da54aa6..3946f9217cfb4 100644 --- a/ios/web/js_messaging/java_script_feature_inttest.mm +++ b/ios/web/js_messaging/java_script_feature_inttest.mm @@ -459,6 +459,46 @@ TEST_F(JavaScriptFeaturePrivateTest, JavaScriptFeatureDoNotInjectJavaScript) { web_state(), kFakeJavaScriptFeatureLoadedText, base::Seconds(1))); } +// Demonstrates that the MakeScriptPrivate origin gate can be bypassed from a +// non-allowlisted origin by poisoning Array.prototype.includes and forcing +// WKUserScript re-injection via document.open(). The private FeatureScript +// (kPageContentWorld + kValidTestOriginForTesting, allowlist = https://test.test) +// must NOT execute on http://invalid.test, but does after the bypass. +// +// This test is expected to FAIL (i.e. the bypass succeeds) until +// MakeScriptPrivate stops relying on page-mutable Array.prototype.includes, +// or kPageContentWorld + non-public OriginFilter is forbidden. +TEST_F(JavaScriptFeaturePrivateTest, + OriginGateBypassViaPrototypePoisoningAndDocumentOpen) { + // 1. Load a page on a NON-allowlisted origin. The kDocumentStart gate + // `['https://test.test'].includes(window.location.origin)` evaluates + // using the pristine built-in and returns false; the private body and + // its `_injected_` sentinel are skipped. + LoadHtml(kPageHTML, GURL("http://invalid.test")); + ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents1")); + ASSERT_FALSE(test::WaitForWebViewContainingText( + web_state(), kFakeJavaScriptFeatureLoadedText, base::Seconds(1))); + + // 2. Page script poisons Array.prototype.includes (writable+configurable) + // in the page content world, then replaces the document. WebKit + // re-evaluates every kDocumentStart WKUserScript in the SAME JS realm + // (see ios/web/js_messaging/page_script_util.h), so the gate now calls + // the poisoned includes() and returns true. The inner + // MakeScriptInjectableOnce sentinel was never set, so the private body + // runs on http://invalid.test. + ASSERT_TRUE(ExecuteJavaScript( + @"Array.prototype.includes = function() { return true; };" + "document.open();" + "document.write('
contents1
" + "');" + "document.close(); true;")); + + // 3. Bypass proof: the private script appended `injected_script_loaded`. + EXPECT_FALSE(test::WaitForWebViewContainingText( + web_state(), kFakeJavaScriptFeatureLoadedText, base::Seconds(2))) + << "MakeScriptPrivate origin gate bypassed on non-allowlisted origin"; +} + // Tests that a private JavaScriptFeature can call JavaScript when on an // authorized page. TEST_F(JavaScriptFeaturePrivateTest, CallFunctionOnAllowedPage) {