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']);
+ });
+});