Skip to content

Fix: ReactDevTool crashes if we have sandbox iframe in props #19994

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

Closed
wants to merge 3 commits into from

Conversation

omarsy
Copy link
Contributor

@omarsy omarsy commented Oct 9, 2020

Summary

When we have an iframe in the props it causes a crash in reactdevtool.
You can see here a demo of the crash: https://codesandbox.io/s/modest-moser-jg9x9?file=/src/index.js.
Before:

image

After:
image

Maybe correct this issue #19854 (comment)

Test Plan

@codesandbox-ci
Copy link

codesandbox-ci bot commented Oct 9, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 77bac8c:

Sandbox Source
React Configuration
unruffled-microservice-jo8s2 PR

@sizebot
Copy link

sizebot commented Oct 9, 2020

Warnings
⚠️ Could not find build artifacts for base commit: 4890779

Size changes (stable)

Generated by 🚫 dangerJS against 77bac8c

@sizebot
Copy link

sizebot commented Oct 9, 2020

Warnings
⚠️ Could not find build artifacts for base commit: 4890779

Size changes (experimental)

Generated by 🚫 dangerJS against 77bac8c

@bvaughn
Copy link
Contributor

bvaughn commented Oct 16, 2020

Thanks for the PR @omarsy. I have a few questions though before moving forward.

Comment on lines 298 to 312
case 'window':
try {
// eslint-disable-next-line no-unused-expressions
data.origin;
} catch {
cleaned.push(path);
return {
inspectable: false,
preview_short: 'Window',
preview_long: 'Window',
name: 'Window',
type,
};
}
// eslint-disable-next-line no-fallthrough
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should use the fall through here. Even with the explicit lint rule, it's hard to read and would be easy to break.

Why shouldn't we return the window object regardless of the try/catch?

If we do need the current behavior for a reason I'm not seeing, can we rewrite to make it more explicit, e.g.

    case 'object':
    case 'window':
      if (type === 'window') {
        try {
          // eslint-disable-next-line no-unused-expressions
          data.origin;
        } catch {
          cleaned.push(path);
          return {
            inspectable: false,
            preview_short: 'Window',
            preview_long: 'Window',
            name: 'Window',
            type,
          };
        }
      }

      isPathAllowedCheck = isPathAllowed(path);
      if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
        return createDehydrated(type, true, data, cleaned, path);
      } else {
        const object = {};
        getAllEnumerableKeys(data).forEach(key => {
          const name = key.toString();
          object[name] = dehydrate(
            data[key],
            cleaned,
            unserializable,
            path.concat([name]),
            isPathAllowed,
            isPathAllowedCheck ? 1 : level + 1,
          );
        });
        return object;
      }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why shouldn't we return the window object regardless of the try/catch?

I want to keep the behaviour we had before. We used to display all the elements of the Window object when we could access it.

@@ -325,7 +339,6 @@ export function dehydrate(
return {
type,
};

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Keep the empty line on 298 and 328 for consistency with the rest of the file 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

:-D Reverted

Comment on lines +467 to +469
if (data.self === data && data.parent && data.top) {
return 'window';
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like an convoluted way to check for window objects. Why wouldn't something like data instanceof Window work?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm also a little confused about why this fix (for window elements) is required to fix the linked bug about iframes. Wouldn't we need to be adding a check for data instanceof HTMLIFrameElement here instead of window?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hello @bvaughn

This seems like an convoluted way to check for window objects. Why wouldn't something like data instanceof Window work?

It doesn't always work for Iframe it considers that sometimes it is an Object.
I've been looking at how the others are dealing with it:
JQuery: https://api.jquery.com/jquery.iswindow/
Anglular: https://github.com/angular/angular.js/blob/master/src/Angular.js#L677
I added some check to make it more robust but i flow the same behaviour.

I'm also a little confused about why this fix (for window elements) is required to fix the linked bug about iframes. Wouldn't we need to be adding a check for data instanceof HTMLIFrameElement here instead of window?

I tested with the iframe element. It works

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I understand. This check doesn't really look like the two you linked to (JQuery and Angular). It also looks like it could hit a false positive, e.g.

// This would be mistake as type "window"
const fake = {
  parent: 1,
  top: 1,
  // ...
};
fake.self = fake;

But it wouldn't if the check was:

if (data instanceof Window || data instanceof HTMLIFrameElement) {
  return 'window';
}

So I'm curious why this doesn't work?

The JQuery function you linked to has been deprecated since v3.3 which is almost 3 years old, and the Angular check you linked to looks like it's in Angular v1 which is also deprecated.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay I think I've come around to the necessity of this "looks like a window" check, although I think we need an inline comment here explaining the reason we check this way (aka the iframe.contentWindow case).

Maybe we could also handle the more common case explicitly? I dunno, maybe something like this?

// We fallback to duck typing to handle an edge case:
// An HTMLIFrameElement's "contentWindow" is not an instance of Window,
// but rather an instance of the iframe's own Window (which we can't reliably access).
if (
  data instanceof Window ||
  data instanceof HTMLIFrameElement ||
  (data.self === data && data.parent != null && data.top != null)
) {
  return "window";
}

Also, maybe this whole conditional should be moved down into the case 'object': block.

@omarsy
Copy link
Contributor Author

omarsy commented Nov 13, 2020

Sorry @bvaughn for the delay.

@omarsy omarsy requested a review from bvaughn November 13, 2020 19:47
// Todo: Uncomment if this feature is implemented https://github.com/jsdom/jsdom/issues/1892
// const iframe = document.createElement('iframe');
// iframe.setAttribute('sandbox', '');
// window.document.body.appendChild(iframe);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add something like this as a fixture test in the meanwhile? (It doesn't seem too likely that JSDom will implement this.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok I will add the test :)

Comment on lines +467 to +469
if (data.self === data && data.parent && data.top) {
return 'window';
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I understand. This check doesn't really look like the two you linked to (JQuery and Angular). It also looks like it could hit a false positive, e.g.

// This would be mistake as type "window"
const fake = {
  parent: 1,
  top: 1,
  // ...
};
fake.self = fake;

But it wouldn't if the check was:

if (data instanceof Window || data instanceof HTMLIFrameElement) {
  return 'window';
}

So I'm curious why this doesn't work?

The JQuery function you linked to has been deprecated since v3.3 which is almost 3 years old, and the Angular check you linked to looks like it's in Angular v1 which is also deprecated.

@omarsy
Copy link
Contributor Author

omarsy commented Nov 13, 2020

I'm not sure I understand. This check doesn't really look like the two you linked to (JQuery and Angular). It also looks like it could hit a false positive, e.g.

Yes, I changed it on purpose. To avoid as many false positives as possible. But it is the same idea

But it wouldn't if the check was:

if (data instanceof Window || data instanceof HTMLIFrameElement) {
return 'window';
}
So I'm curious why this doesn't work?

I don't know why browsers only consider the main window as a window type and the window of iframes are considered as objects. That's why I've been looking at how other libs do it.
You can find an example of what it will return here: https://jsfiddle.net/fx3hmu2n/

Copy link
Contributor

@bvaughn bvaughn left a comment

Choose a reason for hiding this comment

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

I see! Thanks for explaining. I was checking the main window or the iframe itself, not the contentWindow.

Ping me once the fixture is ready 😄

if (type === 'window') {
try {
// eslint-disable-next-line no-unused-expressions
data.origin;
Copy link
Contributor

Choose a reason for hiding this comment

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

This could use an inline comment explaining the case(s) it's intended to catch.

@@ -78,7 +78,7 @@ export function getAllEnumerableKeys(
const descriptors = Object.getOwnPropertyDescriptors(current);
currentKeys.forEach(key => {
// $FlowFixMe: key can be a Symbol https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
if (descriptors[key].enumerable) {
if (descriptors[key] && descriptors[key].enumerable) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have an example of where this would have thrown before? (Can we add a test for it to ensure that we don't regress?)

Comment on lines +467 to +469
if (data.self === data && data.parent && data.top) {
return 'window';
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay I think I've come around to the necessity of this "looks like a window" check, although I think we need an inline comment here explaining the reason we check this way (aka the iframe.contentWindow case).

Maybe we could also handle the more common case explicitly? I dunno, maybe something like this?

// We fallback to duck typing to handle an edge case:
// An HTMLIFrameElement's "contentWindow" is not an instance of Window,
// but rather an instance of the iframe's own Window (which we can't reliably access).
if (
  data instanceof Window ||
  data instanceof HTMLIFrameElement ||
  (data.self === data && data.parent != null && data.top != null)
) {
  return "window";
}

Also, maybe this whole conditional should be moved down into the case 'object': block.

@stale
Copy link

stale bot commented Jan 9, 2022

This pull request has been automatically marked as stale. If this pull request is still relevant, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize reviewing it yet. Your contribution is very much appreciated.

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label Jan 9, 2022
@omarsy
Copy link
Contributor Author

omarsy commented Jan 9, 2022

bump

@stale stale bot removed the Resolution: Stale Automatically closed due to inactivity label Jan 9, 2022
@sebmarkbage sebmarkbage deleted the branch facebook:master October 20, 2022 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants