fix: respect iframe sandbox flags for external protocol navigation (#50964)

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Sam Attard <sattard@anthropic.com>
This commit is contained in:
trop[bot]
2026-04-11 19:04:01 -07:00
committed by GitHub
parent afbd450ddc
commit 08b9d0a220
2 changed files with 109 additions and 2 deletions

View File

@@ -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;
}

View File

@@ -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('<script>location.href = "magnet:sandbox-test"</script>');
} else {
const sandbox = new URL(req.url!, serverUrl).searchParams.get('sandbox') ?? '';
res.end(`<iframe sandbox="${sandbox}" src="/child"></iframe>`);
}
});
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<void>(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<void>(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']);
});
});