Skip to content
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

Change how AMP inabox validates that the source of a postMessage is valid #12541

Merged
merged 19 commits into from Jan 17, 2018

Conversation

bradfrizzell
Copy link
Contributor

@bradfrizzell bradfrizzell commented Dec 20, 2017

This PR separates out how amp inabox 1. validates that the source of a post message is valid and 2. returns which iframe on the page should be measured. Before, it always looked to measure the direct child frame of the window level at which the host script runs. After this change, it now:

  1. Checks that the source of the postMessage contains one of the registered amp inabox iframes in its parent chain
  2. Either returns the source iframe if it is measureable, or returns whichever xdomain frame of the source's parent hierarchy is measureable.

let tempWin = source.parent;
while (tempWin !== this.win_ && tempWin !== this.win_.top) {
if (tempWin.location.origin !== this.win_.location.origin) {
return null;
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 dev().warn here? Typically we've determined if a frame is cross by attempting to access state within it as opposed to looking at origin.

// If there is, the host doc will not be able to accurately measure
// the creative's positions in the page, so return null.
let tempWin = source.parent;
while (tempWin !== this.win_ && tempWin !== this.win_.top) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's limit the amount of iterations (say 10) as we've seen situations where somehow windows get into an infinite loop. If we hit the limit, return null.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@jonkeller
Copy link
Contributor

Nit: in intro comment, change <iframe src="origin1.biz"> to <iframe src="https://origin1.biz"> so it doesn't look like you're looking for a relative URL of a file called origin1.biz

}
}

function canTouchProperty(obj, prop) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Comment describing method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

// If there is, the host doc will not be able to accurately measure
// the creative's positions in the page, so return null.
// Limit to 10 iterations.
let tempWin = source.parent;
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's only do this if the frame owning the message exists in the array. This will potentially save CPU cycles.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

* @param {Window} win
* @return {boolean}
*/
export function canInspectWindow(win) {
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO, only centralize code if and when there are more than two consumers but your call. If you do decide to leave here, you need explicit test coverage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved

// Limit to 10 iterations.
let tempWin = source.parent;
for (let i = 0; i < 10; i++) {
if (tempWin == this.win_ || tempWin == this.win_.top) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this indentation correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

lint says so.... but I'll try something else!

// the creative's positions in the page, so return null.
// Limit to 10 iterations.
let tempWin = source.parent;
for (let i = 0; i < 10; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You can move the if break statement directly into the for loop declaration, I also think we should make sure tempWin exists:

for (let i = 0; i < 10 && tempWin && tempWin != this.win_ && tempWin != this.win_.top; i++) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor

Choose a reason for hiding this comment

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

BTW if you want to be really fancy you can include the declaration of tempWin:

for (let i = 0, tempWin = source.parent; i < 10 && tempWin && tempWin != this.win_ && tempWin != this.win_.top; i++, tempWin = source.parent) {
if (!canInspectWindow(tempWin)) {
return null;
}
}

Now that I think of it, your loop is not properly returning null when we hit the more than 10 case, so really this should be:

for (let i = 0, tempWin = source.parent; tempWin && tempWin != this.win_ && tempWin != this.win_.top; i++, tempWin = source.parent) {
if (i == 10 || !canInspectWindow(tempWin)) return null;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

break;
}
if (!canInspectWindow(tempWin)) {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

let's dev().warn here preferably with actionable info though I suppose we expect it in the passback case though we are indicating in the ad request that inabox is not eligible. I still wonder what we should do about this case as we need to ensure we meet the same level of viewability measurement support.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

*/
function canInspectWindow(win) {
try {
return !!win && win.location.href != null && canTouchProperty(win, 'test');
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can just be the following. If win or win location do not exist, it will throw which is equivalent:

try {
const /* eslint no-unused-vars: 0 */ unused = win.location.href && win['test'];
return true;
} catch (unusedErr) {
return false;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

// the creative's positions in the page, so return null.
// Limit to 10 iterations.
let tempWin = source.parent;
for (let i = 0; i < 10; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

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

BTW if you want to be really fancy you can include the declaration of tempWin:

for (let i = 0, tempWin = source.parent; i < 10 && tempWin && tempWin != this.win_ && tempWin != this.win_.top; i++, tempWin = source.parent) {
if (!canInspectWindow(tempWin)) {
return null;
}
}

Now that I think of it, your loop is not properly returning null when we hit the more than 10 case, so really this should be:

for (let i = 0, tempWin = source.parent; tempWin && tempWin != this.win_ && tempWin != this.win_.top; i++, tempWin = source.parent) {
if (i == 10 || !canInspectWindow(tempWin)) return null;
}

@lannka
Copy link
Contributor

lannka commented Jan 2, 2018

Currently, the AMP inabox host script does not allow messages to be processed from iframes that are nested, i.e. only a direct child frame of the window level at which the host script is running are allowed.

Didn't look into the code, but the above description isn't very accurate.

The host script does allow messages from nested iframe.

host -> iframe 1 -> iframe 2 -> iframe 3

say if host receives a message from iframe3, it will try to locate iframe 1 by walking up in the window tree.

it will reject the message only if iframe1 is not in the array. that's to say, if you want to accept the message, the ads tag should add iframe1 element to the array.

Copy link
Contributor

@lannka lannka left a comment

Choose a reason for hiding this comment

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

As discussed offline, we can either fix the issue in ads tag side, by always registering the direct child iframe.

Or, we can make the host script more error tolerant, by accepting registration of any level of nested iframe, and measuring the closest friendly iframe ancestor of the message source window.

* is found. Then, it returns the frame element for that window.
* For when win is friendly framed, returns the frame element for win.
* @param {!Window} win
* @return {?Element}
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

getMeasureableFrame(win) {
let measureableWin;
let measureableFrame = win.frameElement;
for (let j = 0, tempWin = win; j < 5 && tempWin != tempWin.top;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why limit to 5? I think 10 is a more reasonable value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason that I limited it to 5 was because the previous design iteration you'd suggested limited to a total depth of 10. The way this is designed now splits up the way we check the depth into being a check of how deeply nested the source frame is from the top-most xdomain frame, and how deeply nested the top-most xdomain frame is from window.top. This 5 here was putting a limit on the depth of the source in regards to the top-most x-domain frame. Fine to move it to 10

let measureableFrame = win.frameElement;
for (let j = 0, tempWin = win; j < 5 && tempWin != tempWin.top;
j++, tempWin = tempWin.parent) {
if (!canInspectWindow(tempWin)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's invert these so you don't have a not condition

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

if (!!measureableWin) {
const parent = measureableWin.parent;
const iframes = parent.document.querySelectorAll('iframe');
for (let k = 0, frame = iframes[k]; k < iframes.length;
Copy link
Contributor

Choose a reason for hiding this comment

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

Some code golf for you, perhaps wrapping in Array is bad for performance... not sure:
Array(measureableWin.parent.document.getElementsByTagName('iframe')).forEach(iframe => frame.contentWindow == measureableWin && measureableFrame = frame);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

gonna meet you halfway on this, and still use querySelectorAll as that already returns a nodelist, and find instead of forEach so that we only run until we actually find the right frame.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

actually nvm can't do find on a nodelist. Was gonna get overly complicated. Left with a regular for loop and made sure it terminates as soon as we find the right frame.

* This functioned is used to determine if a window is cross-domained
* from the perspective of the current window.
* @param {Window} win
* @return {boolean}
Copy link
Contributor

Choose a reason for hiding this comment

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

Make private?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@bradfrizzell bradfrizzell changed the title Allow nested frames to send AMP inabox post messages if intermediate frames are friendly. Change how AMP inabox validates that the source of a postMessage is valid Jan 11, 2018
}
}
}
return win.frameElement;
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 very confused. did you mean measuarableWin.frameElement?

in fact, can we just do:

function getMeasureableFrame(win) {
  if (win.parent === win) {
    return null;
  }
  if (win.frameElement) {
    return win.frameElement;
  }
  return getMeasureableFrame(win.parent);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I should add more comments explaining why this is the way it is as I agree it's pretty confusing

The reason that this is so complicated, is that if you have a window reference to a xdomain frame, you can not do win.frameElement, even if you actually do have access to that window's frame element. So, instead we have to find the top-most x-domain window, then go up to its parent, and query select on that parent's document for all its child iframes, and then loop over them to find the correct frame element by comparing the frame element's contentWindow to the xdomain window we care about.

However, if there is no xdomain iframe, then we can just return the source's frameElement

@bradfrizzell
Copy link
Contributor Author

Manually tested as working.

* @return {boolean}
* @private
*/
function canInspectWindow(win) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Private function names should end in underscore

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

let topXDomainWin;
for (let j = 0, tempWin = win; j < 10 && tempWin != tempWin.top;
j++, tempWin = tempWin.parent) {
if (canInspectWindow(tempWin)) {
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 not just move canInspectWindow into the for loop declaration:

for (let j = 0, tempWin = win; j < 10 && tempWin != tempWin.top && !canInspectWindow(tempWin); topXDomainWin = tempWin, j++, tempWin = tempWin.parent);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done. A loop with no body, I like it.

// that corresponds to topXDomainWin.
if (!!topXDomainWin) {
const parent = topXDomainWin.parent;
const iframes = parent.document.querySelectorAll('iframe');
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to create parent var, can instead just do const iframes = topXDomainWin.parent.document...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

* Returns true if win's properties can be accessed and win is defined.
* This functioned is used to determine if a window is cross-domained
* from the perspective of the current window.
* @param {Window} win
Copy link
Contributor

Choose a reason for hiding this comment

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

!Window?

@lannka lannka merged commit 8a94d59 into master Jan 17, 2018
@lannka lannka deleted the frizz-nested-inabox branch January 17, 2018 17:37
gzgogo pushed a commit to gzgogo/amphtml that referenced this pull request Jan 26, 2018
…alid (ampproject#12541)

* Allow nested frames if they are friendly

* Add tests

* Respond to feedback

* Respond to feedback

* Respond to feedback

* Fix dumb dumb

* small fix

* See...my....test! See my test! Made of real gorilla chest

* Continued work

* More work in progress

* Continued work, added getMeasureableFrame

* WIP - fixing tests

* Change how getFrameElement works

* Fix check-types and tests

* Respond to feedback

* Make getMeasureableFrame clearer

* minor changes

* Respond to feedback

* Respond to feedback
RanAbram pushed a commit to RanAbram/amphtml that referenced this pull request Mar 12, 2018
…alid (ampproject#12541)

* Allow nested frames if they are friendly

* Add tests

* Respond to feedback

* Respond to feedback

* Respond to feedback

* Fix dumb dumb

* small fix

* See...my....test! See my test! Made of real gorilla chest

* Continued work

* More work in progress

* Continued work, added getMeasureableFrame

* WIP - fixing tests

* Change how getFrameElement works

* Fix check-types and tests

* Respond to feedback

* Make getMeasureableFrame clearer

* minor changes

* Respond to feedback

* Respond to feedback
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.

None yet

6 participants