fix: propagate requesting frame through sync permission checks (#50686)

WebContentsPermissionHelper::CheckPermission was hardcoding
GetPrimaryMainFrame() and deriving the requesting origin from
web_contents_->GetLastCommittedURL(), so the setPermissionCheckHandler
callback always received the top frame's origin and
details.isMainFrame/details.requestingUrl always reflected the main
frame, even when a cross-origin subframe with allow="serial" or
allow="camera; microphone" triggered the check.

Thread the requesting RenderFrameHost through CheckPermission,
CheckSerialAccessPermission, and CheckMediaAccessPermission so the
permission manager receives the real requesting frame. Update the
serial delegate and WebContents::CheckMediaAccessPermission callers to
pass the frame they already have.

Adds a regression test that loads a cross-origin iframe with
allow="camera; microphone", calls enumerateDevices() from within the
iframe, and asserts the permission check handler receives the iframe
origin for requestingOrigin, isMainFrame, and requestingUrl.

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-05 00:43:42 +00:00
committed by GitHub
parent 5b6b1c1641
commit be53e0a470
5 changed files with 74 additions and 14 deletions

View File

@@ -1767,7 +1767,8 @@ bool WebContents::CheckMediaAccessPermission(
content::WebContents::FromRenderFrameHost(render_frame_host);
auto* permission_helper =
WebContentsPermissionHelper::FromWebContents(web_contents);
return permission_helper->CheckMediaAccessPermission(security_origin, type);
return permission_helper->CheckMediaAccessPermission(render_frame_host,
security_origin, type);
}
void WebContents::RequestMediaAccessPermission(

View File

@@ -51,8 +51,7 @@ bool ElectronSerialDelegate::CanRequestPortPermission(
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
auto* permission_helper =
WebContentsPermissionHelper::FromWebContents(web_contents);
return permission_helper->CheckSerialAccessPermission(
frame->GetLastCommittedOrigin());
return permission_helper->CheckSerialAccessPermission(frame);
}
bool ElectronSerialDelegate::HasPortPermission(

View File

@@ -228,14 +228,14 @@ void WebContentsPermissionHelper::RequestPermission(
}
bool WebContentsPermissionHelper::CheckPermission(
content::RenderFrameHost* requesting_frame,
blink::PermissionType permission,
base::DictValue details) const {
auto* rfh = web_contents_->GetPrimaryMainFrame();
auto* permission_manager = static_cast<ElectronPermissionManager*>(
web_contents_->GetBrowserContext()->GetPermissionControllerDelegate());
auto origin = web_contents_->GetLastCommittedURL();
return permission_manager->CheckPermissionWithDetails(permission, rfh, origin,
std::move(details));
auto origin = requesting_frame->GetLastCommittedOrigin().GetURL();
return permission_manager->CheckPermissionWithDetails(
permission, requesting_frame, origin, std::move(details));
}
void WebContentsPermissionHelper::RequestFullscreenPermission(
@@ -313,6 +313,7 @@ void WebContentsPermissionHelper::RequestOpenExternalPermission(
}
bool WebContentsPermissionHelper::CheckMediaAccessPermission(
content::RenderFrameHost* requesting_frame,
const url::Origin& security_origin,
blink::mojom::MediaStreamType type) const {
base::DictValue details;
@@ -321,14 +322,16 @@ bool WebContentsPermissionHelper::CheckMediaAccessPermission(
auto blink_type = type == blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE
? blink::PermissionType::AUDIO_CAPTURE
: blink::PermissionType::VIDEO_CAPTURE;
return CheckPermission(blink_type, std::move(details));
return CheckPermission(requesting_frame, blink_type, std::move(details));
}
bool WebContentsPermissionHelper::CheckSerialAccessPermission(
const url::Origin& embedding_origin) const {
content::RenderFrameHost* requesting_frame) const {
base::DictValue details;
details.Set("securityOrigin", embedding_origin.GetURL().spec());
return CheckPermission(blink::PermissionType::SERIAL, std::move(details));
details.Set("securityOrigin",
requesting_frame->GetLastCommittedOrigin().GetURL().spec());
return CheckPermission(requesting_frame, blink::PermissionType::SERIAL,
std::move(details));
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPermissionHelper);

View File

@@ -47,9 +47,11 @@ class WebContentsPermissionHelper
const GURL& url);
// Synchronous Checks
bool CheckMediaAccessPermission(const url::Origin& security_origin,
bool CheckMediaAccessPermission(content::RenderFrameHost* requesting_frame,
const url::Origin& security_origin,
blink::mojom::MediaStreamType type) const;
bool CheckSerialAccessPermission(const url::Origin& embedding_origin) const;
bool CheckSerialAccessPermission(
content::RenderFrameHost* requesting_frame) const;
private:
explicit WebContentsPermissionHelper(content::WebContents* web_contents);
@@ -61,7 +63,8 @@ class WebContentsPermissionHelper
bool user_gesture = false,
base::DictValue details = {});
bool CheckPermission(blink::PermissionType permission,
bool CheckPermission(content::RenderFrameHost* requesting_frame,
blink::PermissionType permission,
base::DictValue details) const;
// TODO(clavin): refactor to use the WebContents provided by the

View File

@@ -1941,6 +1941,60 @@ describe('session module', () => {
expect(handlerDetails!.isMainFrame).to.be.false();
expect(handlerDetails!.embeddingOrigin).to.equal('file:///');
});
it('provides iframe origin as requestingOrigin for media check from cross-origin subFrame', async () => {
const w = new BrowserWindow({
show: false,
webPreferences: {
partition: 'very-temp-permission-handler-media'
}
});
const ses = w.webContents.session;
const iframeUrl = 'https://myfakesite/';
let capturedOrigin: string | undefined;
let capturedIsMainFrame: boolean | undefined;
let capturedRequestingUrl: string | undefined;
let capturedSecurityOrigin: string | undefined;
ses.protocol.interceptStringProtocol('https', (req, cb) => {
cb('<html><body>iframe</body></html>');
});
ses.setPermissionCheckHandler((wc, permission, requestingOrigin, details) => {
if (permission === 'media') {
capturedOrigin = requestingOrigin;
capturedIsMainFrame = details.isMainFrame;
capturedRequestingUrl = details.requestingUrl;
capturedSecurityOrigin = (details as any).securityOrigin;
}
return false;
});
try {
await w.loadFile(path.join(fixtures, 'api', 'blank.html'));
w.webContents.executeJavaScript(`
var iframe = document.createElement('iframe');
iframe.src = '${iframeUrl}';
iframe.allow = 'camera; microphone';
document.body.appendChild(iframe);
null;
`);
const [,, frameProcessId, frameRoutingId] = await once(w.webContents, 'did-frame-finish-load');
const frame = webFrameMain.fromId(frameProcessId, frameRoutingId)!;
await frame.executeJavaScript(
'navigator.mediaDevices.enumerateDevices().then(() => {}).catch(() => {});',
true
);
expect(capturedOrigin).to.equal(iframeUrl);
expect(capturedIsMainFrame).to.be.false();
expect(capturedRequestingUrl).to.equal(iframeUrl);
expect(capturedSecurityOrigin).to.equal(iframeUrl);
} finally {
ses.protocol.uninterceptProtocol('https');
ses.setPermissionCheckHandler(null);
}
});
});
describe('ses.isPersistent()', () => {