-
Notifications
You must be signed in to change notification settings - Fork 47k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(react-dom): access iframe contentWindow instead of contentDocument #15099
Conversation
Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign up at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need the corporate CLA signed. If you have received this in error or have any questions, please contact us at [email protected]. Thanks! |
c33a479
to
2aabdf5
Compare
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks! |
How did you test this change? Specifically, how do you know this doesn't regress the use cases for which this logic was added? |
Is there any specific use case that should be tested? Both solutions are trying to get a reference from the window object but the current one has a security violation: try {
win = element.contentDocument.defaultView
// ^ contentDocument will violate the same origin policy if the iframe is in a different domain
} catch (err) {
return element; // the catch block will not be executed because we had a security violation
} |
If you have a same origin policy, does it really matter which API you use? I'd expect that the browser makes sure that no API allows access to the frame contents in that case.
We need to test this change across a variety of browser to ensure we do not regress one (especially since the previous comment was pointing out issues with the API that you're now using). Does this change make it work for Safari on iOS for you? |
In this case it matters because the iframe has a different origin from the parent page, thus it cannot access the content document. For cross origin access you can use the
Applying this changes fixed the problem on Safari. The application that I'm currently working is creating each credit card input in different iframe and relies on post message. This approach didn't work on iOS and the console is bloated by security error messages. A workaround for this issue was to replace some post message logic by directly calling global objects on the window object from each iframe (which introduced some complexity). Steps to reproduce: The step 5 doesn't occur on Chrome 72 and Firefox 65.0.1. Unfortunately codesanbox doesn't support IE 11 but I can provide some video if needed. |
@renanvalentin Thanks for explaining! Interestingly if I use your example and select the iframe manually to call In your initial ticket you mention that this is "breaking the post message flow". What exactly do you mean by that? As far as I can see this is only about an error logged in the browser. |
@renanvalentin You're right I think I made a mistake in selecting the wrong element as In #12037 we also added a new DOM fixture - This is an example that you can build locally by checking out the React repo, running After your explanation I feel confident about your proposed change but we need to make sure that this is not causing any regressions on other browsers (e.g. maybe some older IE versions don't implement Thank you! We really appreciate your help here. |
@p-jackson I've just found an issue and now the selection behaviour is broken: while (element instanceof win.HTMLIFrameElement) {
try {
win = element.contentWindow;
} catch (e) {
return element;
}
element = getActiveElement(win.document); // Can't access the document here
} I'm going to run more tests and get back to you. |
ReactDOM: size: 🔺+0.1%, gzip: 🔺+0.1% Details of bundled changes.Comparing: 103378b...07346f6 react-dom
Generated by 🚫 dangerJS |
c20f858
to
a77230a
Compare
@philipp-spiess now it is working fine and I think that I have a better understanding of this issue now. Safari will throw an uncatchable error when the access results in "Blocked a frame with origin". e.g: A better way is to access one of the cross origin properties: Window or Location Also, evaluating the iframe using console.log will result in "Blocked a frame with origin". e.g: export default function getActiveElement(doc: ?Document): ?Element {
doc = doc || (typeof document !== 'undefined' ? document : undefined);
if (typeof doc === 'undefined') {
return null;
}
try {
console.log(doc.activeElement) // throws an error on console when it is an iframe
return doc.activeElement || doc.body;
} catch (e) {
return doc.body;
}
}
// > Blocked a frame with origin X from accessing a frame with origin Y. Protocols, domains, and ports must match.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall the change looks good but I found out that it doesn’t work for older versions of Safari Desktop. There, it still behaves the same as before the change.
I conducted the following tests using a new fixture that I wrote (we can merge it into this PR if it makes sense) and the existing selection fixtures:
- Chrome - Desktop: 49 ✅, Latest ✅
- Chrome - Android: Latest ✅
- Firefox Desktop: ESR ✅, Latest ✅
- Internet Explorer: 9 ✅, 10 ✅, 11 ✅
- Microsoft Edge: 14
⚠️ , 15 ✅, Latest ✅ - Safari - Desktop: 7 ❌, 8 ❌, 9 ❌, 10 ✅, 11 ✅,Latest ✅
- Safari - iOS: 7 ✅, Latest ✅
I think a unit test for this would not be worth it as we’d have to mock so much that we end up having no margin to change the implementation without the test failing. The good comment should help to prevent accidental regression.
I have two smaller nits inline. Thank you a ton for finding this workaround!
try { | ||
// Accessing the contentDocument of a HTMLIframeElement can cause the browser | ||
// to throw, e.g. if it has a cross-origin src attribute. | ||
// Safari will throw an uncatchable error when the access results in "Blocked a frame with origin". e.g: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the term "uncatchable error" is misleading since the error can be caught, it's just that in addition to the error, Safari will also show an error in the console.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason I had this problem of having the catch block not being executed but I couldn't reproduce it anymore.
I changed the comment to Safari will show an error in the console when the access results in "Blocked a frame with origin"
// https://html.spec.whatwg.org/multipage/browsers.html#integration-with-idl | ||
|
||
const canReadHrefAttribute = iframe.contentWindow.location.href; | ||
return canReadHrefAttribute != null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to find out wether this returns null
or undefined
but it should always be a string from what I can see so maybe we check for this instead?
return typeof iframe.contentWindow.location.href === 'string';
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated to use your suggestion. I wasn't sure how to express this code, something like this also came up to my mind:
(iframe.contentWindow.location.href) // trying to evaluate the expression
return true;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I was thinking the same but I'm not sure if GCC would strip this out so I think the typeof
there makes more sense. Thanks for the update!
5df7ff7
to
8e367f1
Compare
MDN has a list of methods for obtaining the window reference of an iframe: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Syntax fix(react-dom): check if iframe belongs to the same origin Accessing the contentDocument of a HTMLIframeElement can cause the browser to throw, e.g. if it has a cross-origin src attribute. Safari will show an error in the console when the access results in "Blocked a frame with origin". e.g: ```javascript try { $0.contentDocument.defaultView } catch (err) { console.log('err', err) } > Blocked a frame with origin X from accessing a frame with origin Y. Protocols, domains, and ports must match. > err – TypeError: null is not an object (evaluating '$0.contentDocument.defaultView') ``` A safety way is to access one of the cross origin properties: Window or Location Which might result in "SecurityError" DOM Exception and it is compatible to Safari. ```javascript try { $0.contentWindow.location.href } catch (err) { console.log('err', err) } > err – SecurityError: Blocked a frame with origin "http://localhost:3001" from accessing a cross-origin frame. Protocols, domains, and ports must match. ``` https://html.spec.whatwg.org/multipage/browsers.html#integration-with-idl
8e367f1
to
07346f6
Compare
@philipp-spiess Thanks for sharing those tests results, this issue had more impact on mobile than desktop. We can also merge the fixture into this PR since it will help to reproduce this issue in the future. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The diff looks good to me. Thank you!
@gaearon do you want to take a quick look? Browser tests can be found here. There are some problems with older versions of Desktop Safari but given that a lot of people ran into this already I think it still makes sense to get this in.
I trust you this works. It's been an annoying issue so let's get it in. Thank you both. |
facebook#15099) MDN has a list of methods for obtaining the window reference of an iframe: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Syntax fix(react-dom): check if iframe belongs to the same origin Accessing the contentDocument of a HTMLIframeElement can cause the browser to throw, e.g. if it has a cross-origin src attribute. Safari will show an error in the console when the access results in "Blocked a frame with origin". e.g: ```javascript try { $0.contentDocument.defaultView } catch (err) { console.log('err', err) } > Blocked a frame with origin X from accessing a frame with origin Y. Protocols, domains, and ports must match. > err – TypeError: null is not an object (evaluating '$0.contentDocument.defaultView') ``` A safety way is to access one of the cross origin properties: Window or Location Which might result in "SecurityError" DOM Exception and it is compatible to Safari. ```javascript try { $0.contentWindow.location.href } catch (err) { console.log('err', err) } > err – SecurityError: Blocked a frame with origin "http://localhost:3001" from accessing a cross-origin frame. Protocols, domains, and ports must match. ``` https://html.spec.whatwg.org/multipage/browsers.html#integration-with-idl
MDN has a list of methods for obtaining the window reference of an
iframe:
https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Syntax
Closes #14002