diff --git a/chrome/browser/web_applications/web_app_scope_unittest.cc b/chrome/browser/web_applications/web_app_scope_unittest.cc index 509c10b50c2ad..7d05863a2fcbc 100644 --- a/chrome/browser/web_applications/web_app_scope_unittest.cc +++ b/chrome/browser/web_applications/web_app_scope_unittest.cc @@ -141,6 +141,52 @@ TEST_F(WebAppScopeTest, IsInScopeWithScopeExtensions) { EXPECT_FALSE(web_app_scope->IsInScope(GURL("https://another.com/"))); } +// POC: Demonstrates that the has_origin_wildcard branch of +// GetScopeExtensionsScore() drops the port on both sides of the host-suffix +// comparison. A scope_extension validated for https://example.com:8080 (the +// only origin the attacker controls) ends up matching default-port-443 URLs +// on subdomains the attacker does *not* control. +TEST_F(WebAppScopeTest, WildcardScopeExtensionIgnoresPort_POC) { + // Attacker's PWA is hosted on (and validated against) port 8080 only. + const GURL app_scope("https://example.com:8080/"); + + // This is the ScopeExtensionInfo the browser stores after parsing + // "scope_extensions":[{"type":"origin","origin":"https://*.example.com:8080"}] + // and successfully fetching .well-known/web-app-origin-association from + // https://example.com:8080 (which the attacker controls). + const std::vector scope_extensions = { + ScopeExtensionInfo::CreateForOrigin( + url::Origin::CreateFromNormalizedTuple("https", "example.com", 8080), + /*has_origin_wildcard=*/true)}; + + webapps::AppId app_id = + InstallWebAppWithScopeExtensions(app_scope, scope_extensions); + std::optional web_app_scope = + registrar().GetEffectiveScope(app_id); + ASSERT_TRUE(web_app_scope); + + // Victim navigates to the legitimate site on default port 443. + const GURL victim_443("https://sub.example.com/reset?token=SECRET"); + + // BUG: this should be false (port 443 != port 8080), but the wildcard + // matcher compares origin.Serialize() (no ":443") against + // scope_extension.origin.host() (no port at all), so it matches. + // EXPECT_FALSE here documents correct behavior; the test FAILS, proving + // the vulnerability. + EXPECT_FALSE(web_app_scope->IsInScope(victim_443)) + << "Port-443 URL incorrectly considered in-scope of a port-8080 " + "wildcard scope_extension. Link-capturing / launchQueue would " + "deliver this URL (incl. token) to the attacker's PWA."; + + // Secondary observation: the *intended* match (same host, port 8080) is + // actually REJECTED, because Serialize() yields "...:8080" which does not + // EndsWith("example.com"). The matcher inverts the intended semantics. + EXPECT_TRUE( + web_app_scope->IsInScope(GURL("https://sub.example.com:8080/page"))) + << "Port-8080 subdomain (the declared port) was rejected — wildcard " + "matcher is broken in both directions."; +} + TEST_F(WebAppScopeTest, LongerScopeWins) { const GURL scope1("https://example.com/"); const GURL scope2("https://example.com/app/");