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
Conversation
ads/inabox/inabox-messaging-host.js
Outdated
let tempWin = source.parent; | ||
while (tempWin !== this.win_ && tempWin !== this.win_.top) { | ||
if (tempWin.location.origin !== this.win_.location.origin) { | ||
return 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.
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.
ads/inabox/inabox-messaging-host.js
Outdated
// 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) { |
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.
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.
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.
done
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 |
src/3p-frame-messaging.js
Outdated
} | ||
} | ||
|
||
function canTouchProperty(obj, prop) { |
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.
Nit: Comment describing method
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.
done
ads/inabox/inabox-messaging-host.js
Outdated
// 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; |
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.
Let's only do this if the frame owning the message exists in the array. This will potentially save CPU cycles.
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.
done
src/3p-frame-messaging.js
Outdated
* @param {Window} win | ||
* @return {boolean} | ||
*/ | ||
export function canInspectWindow(win) { |
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.
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.
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.
moved
ads/inabox/inabox-messaging-host.js
Outdated
// Limit to 10 iterations. | ||
let tempWin = source.parent; | ||
for (let i = 0; i < 10; i++) { | ||
if (tempWin == this.win_ || tempWin == this.win_.top) { |
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.
Is this indentation correct?
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.
lint says so.... but I'll try something else!
ads/inabox/inabox-messaging-host.js
Outdated
// 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++) { |
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.
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++) {
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.
done
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.
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;
}
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.
done
ads/inabox/inabox-messaging-host.js
Outdated
break; | ||
} | ||
if (!canInspectWindow(tempWin)) { | ||
return 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.
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.
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.
done
ads/inabox/inabox-messaging-host.js
Outdated
*/ | ||
function canInspectWindow(win) { | ||
try { | ||
return !!win && win.location.href != null && canTouchProperty(win, 'test'); |
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 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;
}
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.
done
ads/inabox/inabox-messaging-host.js
Outdated
// 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++) { |
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.
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;
}
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. |
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.
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} |
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.
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.
done
ads/inabox/inabox-messaging-host.js
Outdated
getMeasureableFrame(win) { | ||
let measureableWin; | ||
let measureableFrame = win.frameElement; | ||
for (let j = 0, tempWin = win; j < 5 && tempWin != tempWin.top; |
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.
Why limit to 5? I think 10 is a more reasonable value
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 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
ads/inabox/inabox-messaging-host.js
Outdated
let measureableFrame = win.frameElement; | ||
for (let j = 0, tempWin = win; j < 5 && tempWin != tempWin.top; | ||
j++, tempWin = tempWin.parent) { | ||
if (!canInspectWindow(tempWin)) { |
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.
Let's invert these so you don't have a not condition
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.
done
ads/inabox/inabox-messaging-host.js
Outdated
if (!!measureableWin) { | ||
const parent = measureableWin.parent; | ||
const iframes = parent.document.querySelectorAll('iframe'); | ||
for (let k = 0, frame = iframes[k]; k < iframes.length; |
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.
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);
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.
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.
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.
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.
ads/inabox/inabox-messaging-host.js
Outdated
* This functioned is used to determine if a window is cross-domained | ||
* from the perspective of the current window. | ||
* @param {Window} win | ||
* @return {boolean} |
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.
Make private?
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.
done
ads/inabox/inabox-messaging-host.js
Outdated
} | ||
} | ||
} | ||
return win.frameElement; |
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'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);
}
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 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
Manually tested as working. |
ads/inabox/inabox-messaging-host.js
Outdated
* @return {boolean} | ||
* @private | ||
*/ | ||
function canInspectWindow(win) { |
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.
Private function names should end in underscore
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.
done
ads/inabox/inabox-messaging-host.js
Outdated
let topXDomainWin; | ||
for (let j = 0, tempWin = win; j < 10 && tempWin != tempWin.top; | ||
j++, tempWin = tempWin.parent) { | ||
if (canInspectWindow(tempWin)) { |
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.
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);
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.
done. A loop with no body, I like it.
ads/inabox/inabox-messaging-host.js
Outdated
// that corresponds to topXDomainWin. | ||
if (!!topXDomainWin) { | ||
const parent = topXDomainWin.parent; | ||
const iframes = parent.document.querySelectorAll('iframe'); |
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.
No need to create parent var, can instead just do const iframes = topXDomainWin.parent.document...
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.
fixed
ads/inabox/inabox-messaging-host.js
Outdated
* 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 |
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.
!Window?
…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
…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
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: