From 4558da2cc9db8ffd41330fb65ee02d761ae53ec7 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 13 Oct 2016 18:11:00 -0400 Subject: [PATCH 1/5] Allow webviews to be set as trusted viewers Webviews can't set `ancestorOrigins` properly (maybe?), we can't use it to tell if we are in a trusted viewer context. Instead, fall back to our "old browser" path, which creates a `trustedViewerResolver_`. When the webview's integration script [sets the message deliverer](https://github.com/ampproject/amphtml/blob/f28e116/src/service/viewer-impl.js#L947), it [will resolve](https://github.com/ampproject/amphtml/blob/f28e116/src/service/viewer-impl.js#L961-L962) to the webview's passed origin. Fixes #5563. --- src/service/viewer-impl.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/service/viewer-impl.js b/src/service/viewer-impl.js index dda5260fa805..ffb87c302063 100644 --- a/src/service/viewer-impl.js +++ b/src/service/viewer-impl.js @@ -240,12 +240,18 @@ export class Viewer { this.performanceTracking_ = this.params_['csi'] === '1'; dev().fine(TAG_, '- performanceTracking:', this.performanceTracking_); + /** + * Whether the AMP document is embedded in a webview. + * @private @const {boolean} + */ + this.isWebviewEmbedded_ = this.params_['webview']; + /** * Whether the AMP document is embedded in a viewer, such as an iframe or * a web view. * @private @const {boolean} */ - this.isEmbedded_ = (this.isIframed_ || this.params_['webview'] === '1') && + this.isEmbedded_ = (this.isIframed_ || this.isWebviewEmbedded_) && !this.win.AMP_TEST_IFRAME; /** @private {boolean} */ @@ -293,7 +299,7 @@ export class Viewer { // Not embedded in IFrame - can't trust the viewer. trustedViewerResolved = false; trustedViewerPromise = Promise.resolve(false); - } else if (this.win.location.ancestorOrigins) { + } else if (this.win.location.ancestorOrigins && !this.isWebviewEmbedded_) { // Ancestors when available take precedence. This is the main API used // for this determination. Fallback is only done when this API is not // supported by the browser. From 0c4d255fec832b7ac488f7026d79bc48fa6b9b04 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 13 Oct 2016 18:46:22 -0400 Subject: [PATCH 2/5] Add tests --- src/service/viewer-impl.js | 2 +- test/functional/test-viewer.js | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/service/viewer-impl.js b/src/service/viewer-impl.js index ffb87c302063..16f5f7d7243c 100644 --- a/src/service/viewer-impl.js +++ b/src/service/viewer-impl.js @@ -244,7 +244,7 @@ export class Viewer { * Whether the AMP document is embedded in a webview. * @private @const {boolean} */ - this.isWebviewEmbedded_ = this.params_['webview']; + this.isWebviewEmbedded_ = !!this.params_['webview']; /** * Whether the AMP document is embedded in a viewer, such as an iframe or diff --git a/test/functional/test-viewer.js b/test/functional/test-viewer.js index 0dec5dc85248..52a75fdddc07 100644 --- a/test/functional/test-viewer.js +++ b/test/functional/test-viewer.js @@ -838,6 +838,50 @@ describe('Viewer', () => { }); }); + describe('when in webview', () => { + it('should decide trusted on connection with origin', () => { + windowApi.parent = windowApi; + windowApi.location.hash = '#webview=1'; + windowApi.location.ancestorOrigins = []; + viewer.setMessageDeliverer(() => {}, 'https://google.com'); + return viewer.isTrustedViewer().then(res => { + expect(res).to.be.true; + }); + }); + + it('should NOT allow channel without origin', () => { + windowApi.parent = windowApi; + windowApi.location.hash = '#webview=1'; + windowApi.location.ancestorOrigins = []; + const viewer = new Viewer(ampdoc); + expect(() => { + viewer.setMessageDeliverer(() => {}); + }).to.throw(/message channel must have an origin/); + }); + + it('should decide non-trusted on connection with wrong origin', () => { + windowApi.parent = windowApi; + windowApi.location.hash = '#webview=1'; + windowApi.location.ancestorOrigins = []; + const viewer = new Viewer(ampdoc); + viewer.setMessageDeliverer(() => {}, 'https://untrusted.com'); + return viewer.isTrustedViewer().then(res => { + expect(res).to.be.false; + }); + }); + + it('should NOT give precedence to ancestor', () => { + windowApi.parent = windowApi; + windowApi.location.hash = '#webview=1'; + windowApi.location.ancestorOrigins = ['https://google.com']; + const viewer = new Viewer(ampdoc); + viewer.setMessageDeliverer(() => {}, 'https://untrusted.com'); + return viewer.isTrustedViewer().then(res => { + expect(res).to.be.false; + }); + }); + }); + it('should trust domain variations', () => { test('https://google.com', true); test('https://www.google.com', true); From 9ee30044092f4365d76f82d3ec76abe345a82f67 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 13 Oct 2016 18:59:41 -0400 Subject: [PATCH 3/5] Do not trust "webviews" that are really bad actor iframes We treat them like normal iframes. --- src/service/viewer-impl.js | 2 +- test/functional/test-viewer.js | 72 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/service/viewer-impl.js b/src/service/viewer-impl.js index 16f5f7d7243c..53d73ed3879a 100644 --- a/src/service/viewer-impl.js +++ b/src/service/viewer-impl.js @@ -244,7 +244,7 @@ export class Viewer { * Whether the AMP document is embedded in a webview. * @private @const {boolean} */ - this.isWebviewEmbedded_ = !!this.params_['webview']; + this.isWebviewEmbedded_ = !this.isIframed_ && !!this.params_['webview']; /** * Whether the AMP document is embedded in a viewer, such as an iframe or diff --git a/test/functional/test-viewer.js b/test/functional/test-viewer.js index 52a75fdddc07..0a542ec757eb 100644 --- a/test/functional/test-viewer.js +++ b/test/functional/test-viewer.js @@ -882,6 +882,78 @@ describe('Viewer', () => { }); }); + describe('when in a fake webview (a bad actor iframe)', () => { + it('should consider trusted by ancestor', () => { + windowApi.parent = {}; + windowApi.location.hash = '#webview=1'; + windowApi.location.ancestorOrigins = ['https://google.com']; + return new Viewer(ampdoc).isTrustedViewer().then(res => { + expect(res).to.be.true; + }); + }); + + it('should consider non-trusted without ancestor', () => { + windowApi.parent = {}; + windowApi.location.hash = '#webview=1'; + windowApi.location.ancestorOrigins = []; + return new Viewer(ampdoc).isTrustedViewer().then(res => { + expect(res).to.be.false; + }); + }); + + it('should consider non-trusted with wrong ancestor', () => { + windowApi.parent = {}; + windowApi.location.hash = '#webview=1'; + windowApi.location.ancestorOrigins = ['https://untrusted.com']; + return new Viewer(ampdoc).isTrustedViewer().then(res => { + expect(res).to.be.false; + }); + }); + + it('should decide trusted on connection with origin', () => { + windowApi.parent = {}; + windowApi.location.hash = '#webview=1'; + windowApi.location.ancestorOrigins = null; + const viewer = new Viewer(ampdoc); + viewer.setMessageDeliverer(() => {}, 'https://google.com'); + return viewer.isTrustedViewer().then(res => { + expect(res).to.be.true; + }); + }); + + it('should NOT allow channel without origin', () => { + windowApi.parent = {}; + windowApi.location.hash = '#webview=1'; + windowApi.location.ancestorOrigins = null; + const viewer = new Viewer(ampdoc); + expect(() => { + viewer.setMessageDeliverer(() => {}); + }).to.throw(/message channel must have an origin/); + }); + + it('should decide non-trusted on connection with wrong origin', () => { + windowApi.parent = {}; + windowApi.location.hash = '#webview=1'; + windowApi.location.ancestorOrigins = null; + const viewer = new Viewer(ampdoc); + viewer.setMessageDeliverer(() => {}, 'https://untrusted.com'); + return viewer.isTrustedViewer().then(res => { + expect(res).to.be.false; + }); + }); + + it('should give precedence to ancestor', () => { + windowApi.parent = {}; + windowApi.location.hash = '#webview=1'; + windowApi.location.ancestorOrigins = ['https://google.com']; + const viewer = new Viewer(ampdoc); + viewer.setMessageDeliverer(() => {}, 'https://untrusted.com'); + return viewer.isTrustedViewer().then(res => { + expect(res).to.be.true; + }); + }); + }); + it('should trust domain variations', () => { test('https://google.com', true); test('https://www.google.com', true); From 479470f2e64f25c0dc721ee0900f22cf42e6f4f6 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 13 Oct 2016 19:11:06 -0400 Subject: [PATCH 4/5] Test for '1' explicitly --- src/service/viewer-impl.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/service/viewer-impl.js b/src/service/viewer-impl.js index 53d73ed3879a..60cc7bc0318f 100644 --- a/src/service/viewer-impl.js +++ b/src/service/viewer-impl.js @@ -244,7 +244,8 @@ export class Viewer { * Whether the AMP document is embedded in a webview. * @private @const {boolean} */ - this.isWebviewEmbedded_ = !this.isIframed_ && !!this.params_['webview']; + this.isWebviewEmbedded_ = !this.isIframed_ && + this.params_['webview'] == '1'; /** * Whether the AMP document is embedded in a viewer, such as an iframe or From c2851f7f6b2c6b252d6c2a5060dd342dd4b5ef37 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Fri, 14 Oct 2016 13:39:08 -0400 Subject: [PATCH 5/5] Fix test --- test/functional/test-viewer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/test-viewer.js b/test/functional/test-viewer.js index 0a542ec757eb..9bef51bd5288 100644 --- a/test/functional/test-viewer.js +++ b/test/functional/test-viewer.js @@ -843,6 +843,7 @@ describe('Viewer', () => { windowApi.parent = windowApi; windowApi.location.hash = '#webview=1'; windowApi.location.ancestorOrigins = []; + const viewer = new Viewer(ampdoc); viewer.setMessageDeliverer(() => {}, 'https://google.com'); return viewer.isTrustedViewer().then(res => { expect(res).to.be.true;