diff --git a/shell/browser/electron_browser_client.cc b/shell/browser/electron_browser_client.cc index 5426960fb4..85b537dc84 100644 --- a/shell/browser/electron_browser_client.cc +++ b/shell/browser/electron_browser_client.cc @@ -72,6 +72,7 @@ #include "services/network/public/cpp/resource_request_body.h" #include "services/network/public/cpp/self_deleting_url_loader_factory.h" #include "services/network/public/cpp/url_loader_factory_builder.h" +#include "services/network/public/cpp/web_sandbox_flags.h" #include "shell/app/electron_crash_reporter_client.h" #include "shell/browser/api/electron_api_app.h" #include "shell/browser/api/electron_api_crash_reporter.h" @@ -128,6 +129,7 @@ #include "third_party/blink/public/common/tokens/tokens.h" #include "third_party/blink/public/common/web_preferences/web_preferences.h" #include "third_party/blink/public/mojom/badging/badging.mojom.h" +#include "third_party/blink/public/mojom/devtools/console_message.mojom.h" #include "ui/base/resource/resource_bundle.h" #include "ui/native_theme/native_theme.h" #include "v8/include/v8.h" @@ -926,7 +928,9 @@ void HandleExternalProtocolInUI( const GURL& url, content::WeakDocumentPtr document_ptr, content::WebContents::OnceGetter web_contents_getter, - bool has_user_gesture) { + bool has_user_gesture, + bool is_primary_main_frame, + network::mojom::WebSandboxFlags sandbox_flags) { content::WebContents* web_contents = std::move(web_contents_getter).Run(); if (!web_contents) return; @@ -945,6 +949,30 @@ void HandleExternalProtocolInUI( rfh = web_contents->GetPrimaryMainFrame(); } + // Sandboxed iframes without one of the appropriate sandbox-escape tokens + // must not be able to launch external protocol handlers. This mirrors + // chrome/browser/chrome_content_browser_client.cc; see crbug.com/1148777. + if (!is_primary_main_frame) { + using SandboxFlags = network::mojom::WebSandboxFlags; + auto allow = [sandbox_flags](SandboxFlags flag) { + return (sandbox_flags & flag) == SandboxFlags::kNone; + }; + const bool allowed = allow(SandboxFlags::kTopNavigationToCustomProtocols) || + (allow(SandboxFlags::kTopNavigationByUserActivation) && + has_user_gesture); + if (!allowed) { + rfh->AddMessageToConsole( + blink::mojom::ConsoleMessageLevel::kError, + "Navigation to external protocol blocked by sandbox, because it " + "doesn't contain any of: " + "'allow-top-navigation-to-custom-protocols', " + "'allow-top-navigation-by-user-activation', " + "'allow-top-navigation', or 'allow-popups'. See " + "https://chromestatus.com/feature/5680742077038592"); + return; + } + } + GURL escaped_url(base::EscapeExternalHandlerValue(url.spec())); auto callback = base::BindOnce(&OnOpenExternal, escaped_url); permission_helper->RequestOpenExternalPermission(rfh, std::move(callback), @@ -973,7 +1001,8 @@ bool ElectronBrowserClient::HandleExternalProtocol( initiator_document ? initiator_document->GetWeakDocumentPtr() : content::WeakDocumentPtr(), - std::move(web_contents_getter), has_user_gesture)); + std::move(web_contents_getter), has_user_gesture, + is_primary_main_frame, sandbox_flags)); return true; } diff --git a/spec/chromium-spec.ts b/spec/chromium-spec.ts index 40cb2a726c..79b5c50d26 100644 --- a/spec/chromium-spec.ts +++ b/spec/chromium-spec.ts @@ -4558,3 +4558,81 @@ describe('navigator.usb', () => { } }); }); + +describe('iframe sandbox external protocols', () => { + let server: http.Server; + let serverUrl: string; + let w: BrowserWindow; + let openExternalRequests: string[]; + + before(async () => { + server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'text/html'); + if (req.url === '/child') { + res.end(''); + } else { + const sandbox = new URL(req.url!, serverUrl).searchParams.get('sandbox') ?? ''; + res.end(``); + } + }); + serverUrl = (await listen(server)).url; + }); + + after(() => { + server.close(); + }); + + beforeEach(() => { + openExternalRequests = []; + w = new BrowserWindow({ show: false }); + w.webContents.session.setPermissionRequestHandler((_wc, permission, callback, details) => { + if (permission === 'openExternal') { + openExternalRequests.push((details as any).externalURL); + } + callback(false); + }); + }); + + afterEach(() => { + w.webContents.session.setPermissionRequestHandler(null); + return closeAllWindows(); + }); + + it('blocks navigation to external protocol from a sandboxed iframe', async () => { + const consoleMessage = once(w.webContents, 'console-message'); + await w.loadURL(`${serverUrl}/?sandbox=${encodeURIComponent('allow-scripts')}`); + const [{ message }] = await consoleMessage; + expect(message).to.match(/external protocol blocked by sandbox/); + expect(openExternalRequests).to.be.empty(); + }); + + it('allows navigation to external protocol with allow-top-navigation-to-custom-protocols', async () => { + const requested = new Promise(resolve => { + w.webContents.session.setPermissionRequestHandler((_wc, permission, callback, details) => { + if (permission === 'openExternal') { + openExternalRequests.push((details as any).externalURL); + resolve(); + } + callback(false); + }); + }); + await w.loadURL(`${serverUrl}/?sandbox=${encodeURIComponent('allow-scripts allow-top-navigation-to-custom-protocols')}`); + await requested; + expect(openExternalRequests).to.deep.equal(['magnet:sandbox-test']); + }); + + it('allows navigation to external protocol with allow-popups', async () => { + const requested = new Promise(resolve => { + w.webContents.session.setPermissionRequestHandler((_wc, permission, callback, details) => { + if (permission === 'openExternal') { + openExternalRequests.push((details as any).externalURL); + resolve(); + } + callback(false); + }); + }); + await w.loadURL(`${serverUrl}/?sandbox=${encodeURIComponent('allow-scripts allow-popups')}`); + await requested; + expect(openExternalRequests).to.deep.equal(['magnet:sandbox-test']); + }); +});