Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Conversation

samuelmaddock
Copy link
Member

@samuelmaddock samuelmaddock commented Aug 23, 2024

Description of Change

Note

This is an alternate proposal to #43456

In our security recommendations guide, we list validating senderFrame of IPCs. In the case of IPCs sent after the unload event, it's possible for senderFrame to point to a different RenderFrameHost than the sender of the IPC.

The process occurs as followed (gist to repro):

  1. WebFrameMain is set to RFH A
  2. Cross-origin navigation begins
  3. WebFrameMain is swapped to RFH B
  4. Window 'unload' listener is emitted in RFH A, now marked as kPendingDeletion
  5. RFH A sends an IPC during the unload listener
  6. IPC is received with senderFrame pointing to RFH B

WebFrameMain internally indexes a RFH by its FrameTreeNode ID. This is based on the design of WebFrameMain being similar to how <iframe> works.


To fix this without any breaking changes, we can introduce a few internal changes:

  • Modify all WebFrameMain lookups by RenderFrameHost to map directly to that RFH
    • If the RFH is not attached to the FrameTreeNode, mark it as detached
  • Return null if event.senderFrame is accessed after the frame has already been disposed
    • This ensures it never points to a differing RFH

The result is that IPCs received while an unload listener is running will have WebFrameMain.detached set, but all of the expected RFH properties will still be accessible. Once the RFH is finally deleted, the WebFrameMain instance will be disposed.

As long as we use dict.Set("frame", rfh) or dict.SetGetter("frame", rfh), it’s guaranteed to point to the correct RFH if immediately accessed after emitted.

ipcMain.on('unload-event', (event) => {
  event.senderFrame; // ✅
});

If accessed asynchronously, it will return null if the RFH has swapped or been destroyed.

ipcMain.on('unload-event', async (event) => {
  await crossOriginNavigationPromise;
  event.senderFrame; // ✅ returns `null` without throwing
  event.senderFrame.url; // ❌ throws due to indexing null
});

Todo:

  • Add isDestroyed() so it's easier to check if WFM was disposed already
  • Ensure mainFrame.ipc.on handlers are called in the case of detached frames
  • Document the important of checking WFM synchronously to ensure URL, etc. hasn't changed. This depends on which design we choose for frame properties.

Checklist

Release Notes

Notes:

  • Added WebFrameMain.detached for frames in an unloading state.
  • Added WebFrameMain.isDestroyed() to determine if a frame has been destroyed.
  • Fixed webFrameMain.fromId(processId, frameId) returning a WebFrameMain instance which doesn't match the given parameters when the frame is unloading.

@electron-cation electron-cation bot added the new-pr 🌱 PR opened recently label Aug 23, 2024
@samuelmaddock samuelmaddock added the semver/minor backwards-compatible functionality label Aug 23, 2024
@electron-cation electron-cation bot removed the new-pr 🌱 PR opened recently label Aug 30, 2024
@samuelmaddock
Copy link
Member Author

Reminder todo for me: mainFrame.ipc.on should receive the detached frame messages. This can be looked up via FrameTreeNode ID when dispatching the emitters.

@MarshallOfSound
Copy link
Member

I don't love introducing APIs that could throw under "normal usage". I think I made this point before but ensuring we attach the correct pinned WebFrameMain information should be doable even after the navigation. We just need to stash the information.

Personally I think the much better end-state is to align on things like our new permission handler API which will just expose an origin, then we suggest folks validate the origin instead of the frame and move on with our lives 🤔

@samuelmaddock
Copy link
Member Author

samuelmaddock commented Sep 12, 2024

I don't love introducing APIs that could throw under "normal usage". I think I made this point before but ensuring we attach the correct pinned WebFrameMain information should be doable even after the navigation. We just need to stash the information.

We currently stash the process and routing IDs associated with the RenderFrameHost. This effectively pins all frame property access.

The current design throws if accessed asynchronously. Previously it returned null (undocumented).

ipcMain.on('unload-event', async (event) => {
  await crossOriginNavigationPromise;
  event.senderFrame; // ❌ likely throws due to initial RFH destroyed
});

An alternate design could be to create an "empty" WebFrameMain which is initialized in a destroyed state.

ipcMain.on('unload-event', async (event) => {
  await crossOriginNavigationPromise;
  event.senderFrame; // ✅ object exists
  event.senderFrame.isDestroyed(); // ✅ true because pinned frame info no longer points to valid RFH
  event.senderFrame.url; // ❌ throws as RFH has been destroyed/uninitialized
});

The only thing I don't like about this design is that it's somewhat misleading since the frame could have swapped. Throwing outright will encourage developers to always access synchronously. Throwing also follows the behavior seen in property access such as frame.url of a destroyed frame.

Personally I think the much better end-state is to align on things like our new permission handler API which will just expose an origin, then we suggest folks validate the origin instead of the frame and move on with our lives 🤔

I agree on the security side of things, we should prefer exposing the origin directly. That's the approach I intend to follow with the permissions changes once I get to proposing the permissions-v2 branch.

This PR is addressing the edge case with unloaded frames which applies to IPCs, permissions, and anywhere else we expose frames as property.

@samuelmaddock
Copy link
Member Author

samuelmaddock commented Sep 12, 2024

The frame property is pinned to a specific RenderFrameHost. When accessed immediately upon emitting an event, the frame is expected to exist. If the property is accessed too late, we can't return the expected frame if it has navigated or been destroyed.

Here are some options we can go with for addressing late access.

Option A: Throw upon access

ipcMain.on('unload-event', async (event) => {
  await crossOriginNavigationPromise;
  event.senderFrame; // ❌ throws due to RFH destroyed
});
  • Matches existing behavior of throwing when accessing properties of destroyed frames (e.g. frame.url).
  • Doesn't require any breaking changes to types.

Option B: Return destroyed/empty WebFrameMain

ipcMain.on('unload-event', async (event) => {
  await crossOriginNavigationPromise;
  event.senderFrame; // ✅ object exists
  event.senderFrame.isDestroyed(); // ✅ true because pinned frame info no longer points to valid RFH
  event.senderFrame.url; // ❌ throws as RFH has been destroyed/uninitialized
});
  • Doesn't require any breaking changes to types.

✅ Option C: Return null

ipcMain.on('unload-event', async (event) => {
  await crossOriginNavigationPromise;
  event.senderFrame; // ✅ returns `null` without throwing
  event.senderFrame.url; // ❌ throws due to indexing null
});
  • This is the current, undocumented behavior.
  • If we document this, it will be a breaking change to the types.

Option D: Eagerly initialize frame property

ipcMain.on('unload-event', async (event) => {
  await crossOriginNavigationPromise;
  event.senderFrame; // ✅ object exists
  event.senderFrame.url; // URL points to navigated page
});

Frame properties are lazily evaluated which led us to this problem. We can have the WebFrameMain instance always be created with events instead of being done lazily. This has the drawback of allocations for each unique WebFrameMain instance emitted.

The frame will also continue to follow navigation which we may want to guide folks away from in cases such as IPCs.


With any of the lazily evaluated designs, we can detect when late access occurs and log a warning/error.

Warning: Frame property was accessed after it navigated or was destroyed. Frames should be accessed immediately upon receiving an event.

@samuelmaddock samuelmaddock requested a review from a team as a code owner September 13, 2024 23:42
@samuelmaddock samuelmaddock force-pushed the feat/detached-webframemain branch 2 times, most recently from 62ef220 to 4fa45ef Compare September 19, 2024 16:14
@samuelmaddock samuelmaddock force-pushed the feat/detached-webframemain branch from 4fa45ef to bc851d4 Compare September 26, 2024 15:41
@nornagon
Copy link
Contributor

API LGTM

@nornagon
Copy link
Contributor

some color to my approval:

  1. we've thought about this a long time. this is addressing a security issue, and I think we need to bias towards landing something here. we've gone through a lot of designs and this one seems pretty minimal in terms of what it adds and what it breaks.
  2. as for checking origin vs checking frame, i agree we should recommend checking origin vs checking frame as it's simpler, and 99% of the time people are just checking the url of the frame anyway :)

Copy link
Member

@itsananderson itsananderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API LGTM

Is it worth adding a short note to the breaking changes doc to mention that a bunch of APIs can now have a null value for the frame property? Even if this won't impact many people at runtime, it could cause a little friction with TypeScript type-checking, so it seems worth a high-level mention.

docs/api/session.md Outdated Show resolved Hide resolved
@samuelmaddock samuelmaddock added the target/33-x-y PR should also be added to the "33-x-y" branch. label Sep 30, 2024
@samuelmaddock samuelmaddock requested a review from a team as a code owner September 30, 2024 17:57
@samuelmaddock
Copy link
Member Author

Is it worth adding a short note to the breaking changes doc to mention that a bunch of APIs can now have a null value for the frame property?

@itsananderson I've added a notice about the change of behavior which mentions the possibility of the frame being null 09e9fba

Copy link
Member

@jkleinsc jkleinsc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API LGTM

@samuelmaddock samuelmaddock force-pushed the feat/detached-webframemain branch from 014a4fd to d520445 Compare September 30, 2024 20:56
Copy link
Member

@VerteDinde VerteDinde left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are no API concerns, the code/approach here seems reasonable to me.

Is there any existing behavior where this could break handling event.senderFrame, where a developer is assuming it won’t return null? I think the answer is no, as we’re handling an edge case here, just asking in case any other existing documentation should be updated with the updated types.

lib/browser/api/web-contents.ts Outdated Show resolved Hide resolved
@samuelmaddock
Copy link
Member Author

Is there any existing behavior where this could break handling event.senderFrame, where a developer is assuming it won’t return null? I think the answer is no, as we’re handling an edge case here, just asking in case any other existing documentation should be updated with the updated types.

@VerteDinde the existing behavior is also to return null, albeit undocumented, so it's only new in the case of late-access after navigations.

samuelmaddock and others added 9 commits October 1, 2024 10:52
fix: throw instead of returning null senderFrame

test: detached frames

fix: ensure IPCs of pending deletion RFHs are dispatched

fix: lookup WFM by FTN ID to dispatch IPCs

feat: add frame.isDestroyed()

return null

fix: return undefined

docs: add null to all frame properties

refactor: option c, return null and emit warning

refactor: add routingId & processId to navigation events

test: null frame property

docs: clarify warning message

better wording

clarify null frame

fix: browserwindow spec
@samuelmaddock samuelmaddock force-pushed the feat/detached-webframemain branch from 4be5d6b to 1bdc5dc Compare October 1, 2024 14:53
@jkleinsc jkleinsc merged commit 8b3d70a into electron:main Oct 11, 2024
88 of 92 checks passed
@release-clerk
Copy link

release-clerk bot commented Oct 11, 2024

Release Notes Persisted

  • Added WebFrameMain.detached for frames in an unloading state.
  • Added WebFrameMain.isDestroyed() to determine if a frame has been destroyed.
  • Fixed webFrameMain.fromId(processId, frameId) returning a WebFrameMain instance which doesn't match the given parameters when the frame is unloading.

@trop
Copy link
Contributor

trop bot commented Oct 11, 2024

I have automatically backported this PR to "33-x-y", please check out #44209

@trop trop bot added in-flight/33-x-y and removed target/33-x-y PR should also be added to the "33-x-y" branch. labels Oct 11, 2024
yangannyx pushed a commit to yangannyx/electron that referenced this pull request Oct 21, 2024
* feat: add WebFrameMain detached property

fix: throw instead of returning null senderFrame

test: detached frames

fix: ensure IPCs of pending deletion RFHs are dispatched

fix: lookup WFM by FTN ID to dispatch IPCs

feat: add frame.isDestroyed()

return null

fix: return undefined

docs: add null to all frame properties

refactor: option c, return null and emit warning

refactor: add routingId & processId to navigation events

test: null frame property

docs: clarify warning message

better wording

clarify null frame

fix: browserwindow spec

* maybe fix 🤷

* fix: use updated util electron#43722

* docs: add notice for frame change of behavior

* docs: clarify why frame properties may be null

* lint

* wip

* fix: content::FrameTreeNodeId lookup and converter

* refactor: avoid holey array deoptimization

---------

Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
yangannyx pushed a commit to yangannyx/electron that referenced this pull request Oct 21, 2024
* feat: add WebFrameMain detached property

fix: throw instead of returning null senderFrame

test: detached frames

fix: ensure IPCs of pending deletion RFHs are dispatched

fix: lookup WFM by FTN ID to dispatch IPCs

feat: add frame.isDestroyed()

return null

fix: return undefined

docs: add null to all frame properties

refactor: option c, return null and emit warning

refactor: add routingId & processId to navigation events

test: null frame property

docs: clarify warning message

better wording

clarify null frame

fix: browserwindow spec

* maybe fix 🤷

* fix: use updated util electron#43722

* docs: add notice for frame change of behavior

* docs: clarify why frame properties may be null

* lint

* wip

* fix: content::FrameTreeNodeId lookup and converter

* refactor: avoid holey array deoptimization

---------

Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
yangannyx pushed a commit to yangannyx/electron that referenced this pull request Oct 21, 2024
* feat: add WebFrameMain detached property

fix: throw instead of returning null senderFrame

test: detached frames

fix: ensure IPCs of pending deletion RFHs are dispatched

fix: lookup WFM by FTN ID to dispatch IPCs

feat: add frame.isDestroyed()

return null

fix: return undefined

docs: add null to all frame properties

refactor: option c, return null and emit warning

refactor: add routingId & processId to navigation events

test: null frame property

docs: clarify warning message

better wording

clarify null frame

fix: browserwindow spec

* maybe fix 🤷

* fix: use updated util electron#43722

* docs: add notice for frame change of behavior

* docs: clarify why frame properties may be null

* lint

* wip

* fix: content::FrameTreeNodeId lookup and converter

* refactor: avoid holey array deoptimization

---------

Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
@samuelmaddock samuelmaddock deleted the feat/detached-webframemain branch October 24, 2024 15:59
@trop trop bot added merged/33-x-y PR was merged to the "33-x-y" branch. and removed in-flight/33-x-y labels Oct 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-review/approved ✅ backport/requested 🗳 merged/33-x-y PR was merged to the "33-x-y" branch. semver/minor backwards-compatible functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.