Skip to content

Commit

Permalink
Merge pull request #2333 from cramforce/origin-check
Browse files Browse the repository at this point in the history
Add argument to `draw3p` for checking that custom iframe is embedded into allowed origin.
  • Loading branch information
cramforce committed Feb 29, 2016
2 parents a1a69ac + 8c45b79 commit e5b705c
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 6 deletions.
47 changes: 45 additions & 2 deletions 3p/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {adtech} from '../ads/adtech';
import {plista} from '../ads/plista';
import {doubleclick} from '../ads/doubleclick';
import {dotandads} from '../ads/dotandads';
import {endsWith} from '../src/string';
import {facebook} from './facebook';
import {flite} from '../ads/flite';
import {manageWin} from './environment';
Expand All @@ -40,7 +41,7 @@ import {nonSensitiveDataPostMessage, listenParent} from './messaging';
import {twitter} from './twitter';
import {yieldmo} from '../ads/yieldmo';
import {computeInMasterFrame, register, run} from '../src/3p';
import {parseUrl} from '../src/url';
import {parseUrl, getSourceUrl} from '../src/url';
import {assert} from '../src/asserts';
import {taboola} from '../ads/taboola';
import {smartadserver} from '../ads/smartadserver';
Expand Down Expand Up @@ -153,15 +154,21 @@ function masterSelection(type) {
* no arguments. Configuration is expected to be modified in-place.
* @param {!Array<string>=} opt_allowed3pTypes List of advertising network
* types you expect.
* @param {!Array<string>=} opt_allowedEmbeddingOrigins List of domain suffixes
* that are allowed to embed this frame.
*/
window.draw3p = function(opt_configCallback, opt_allowed3pTypes) {
window.draw3p = function(opt_configCallback, opt_allowed3pTypes,
opt_allowedEmbeddingOrigins) {
try {
ensureFramed(window);
const data = parseFragment(location.hash);
window.context = data._context;
window.context.location = parseUrl(data._context.location.href);
validateParentOrigin(window, window.context.location);
validateAllowedTypes(window, data.type, opt_allowed3pTypes);
if (opt_allowedEmbeddingOrigins) {
validateAllowedEmbeddingOrigins(window, opt_allowedEmbeddingOrigins);
}
window.context.master = masterSelection(data.type);
window.context.isMaster = window.context.master == window;
window.context.data = data;
Expand Down Expand Up @@ -337,6 +344,42 @@ export function validateAllowedTypes(window, type, allowedTypes) {
'Non-whitelisted 3p type for custom iframe: ' + type);
}

/**
* Check that parent host name was whitelisted.
* @param {!Window} window
* @param {!Array<string>} allowedHostnames Suffixes of allowed host names.
* @visiblefortesting
*/
export function validateAllowedEmbeddingOrigins(window, allowedHostnames) {
if (!window.document.referrer) {
throw new Error('Referrer expected: ' + window.location.href);
}
const ancestors = window.location.ancestorOrigins;
// We prefer the unforgable ancestorOrigins, but referrer is better than
// nothing.
const ancestor = ancestors ? ancestors[0] : window.document.referrer;
let hostname = parseUrl(ancestor).hostname;
const onDefault = hostname == 'cdn.ampproject.org';
if (onDefault) {
// If we are on the cache domain, parse the source hostname from
// the referrer. The referrer is used because it should be
// trustable.
hostname = parseUrl(getSourceUrl(window.document.referrer)).hostname;
}
for (let i = 0; i < allowedHostnames.length; i++) {
// Either the hostname is exactly as whitelisted…
if (allowedHostnames[i] == hostname) {
return;
}
// Or it ends in .$hostname (aka is a sub domain of the whitelisted domain.
if (endsWith(hostname, '.' + allowedHostnames[i])) {
return;
}
}
throw new Error('Invalid embedding hostname: ' + hostname + ' not in '
+ allowedHostnames);
}

/**
* Throws if this window is a top level window.
* @param {!Window} window
Expand Down
10 changes: 9 additions & 1 deletion 3p/remote.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@
</head>
<body style="margin:0">
<div id="c" style="position:absolute;top:0;left:0;bottom:0;right:0;">
<script>draw3p(undefined, ['allowed-ad-type1', 'allowed-ad-type2'])</script>
<script>
draw3p(undefined,
// List of expected amp-ad types.
['allowed-ad-type1', 'allowed-ad-type2']
// List of hostnames that are allowed to embed this change.
// Please also use ALLOW-FROM X-Frame-Options to get security in
// browsers that do not support location.ancestorOrigins.
['your-domain.com']);
</script>
</div>
<script>if (window.docEndCallback) window.docEndCallback()</script>
</body>
Expand Down
6 changes: 3 additions & 3 deletions builtins/amp-ad.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ The `content` attribute of the meta tag is the absolute URL to your copy of the

### Security

Validate incoming data before passing it on to the `draw3p` function, to make sure your iframe only does things it expects to do. This is true, in particular, for ad networks that allow custom JavaScript injection.
**Validate incoming data** before passing it on to the `draw3p` function, to make sure your iframe only does things it expects to do. This is true, in particular, for ad networks that allow custom JavaScript injection.

Iframes should also enforce that they are only iframed into origins that they expect to be iframed into. The origins would be:

Expand All @@ -176,7 +176,7 @@ Iframes should also enforce that they are only iframed into origins that they ex

In the case of the AMP cache you also need to check that the "source origin" (origin of the document served by cdn.ampproject.org) is one of your origins.

Enforcing origins can be done using the (allow-from)[https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options] directive, but this is not supported in all browsers. In Chrome and Safari you can, however, check `location.ancestorOrigins` for the expected origin.
Enforcing origins can be done with the 3rd argument to `draw3p` and must additionally be done using the (allow-from)[https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options] directive for full browser support.

### Enhance incoming ad configuration

Expand All @@ -194,5 +194,5 @@ draw3p(function(config, done) {
setTimeout(function() {
done(config);
}, 100)
});
}, ['allowed-ad-type'], ['your-domain.com']);
```
80 changes: 80 additions & 0 deletions test/functional/test-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
draw3p,
ensureFramed,
validateParentOrigin,
validateAllowedEmbeddingOrigins,
validateAllowedTypes,
parseFragment,
} from '../../3p/integration';
Expand Down Expand Up @@ -287,4 +288,83 @@ describe('3p integration.js', () => {
ensureFramed(win);
}).to.throw(/Must be framed: sentinel/);
});

it('should validateAllowedEmbeddingOrigins: non-cache', () => {
const win = {
document: {
referrer: 'https://should-be-ignored',
},
location: {
ancestorOrigins: ['https://www.foo.com'],
},
};
function invalid(fn) {
expect(fn).to.throw(/Invalid embedding hostname/);
}
validateAllowedEmbeddingOrigins(win, ['foo.com']);
validateAllowedEmbeddingOrigins(win, ['foo.net', 'foo.com']);
validateAllowedEmbeddingOrigins(win, ['www.foo.com']);
invalid(() => validateAllowedEmbeddingOrigins(win, ['bar.com']));
invalid(() => validateAllowedEmbeddingOrigins(win, ['amp.www.foo.com']));
invalid(() => validateAllowedEmbeddingOrigins(win, ['ampwww.foo.com']));
});

it('should validateAllowedEmbeddingOrigins: cache', () => {
const win = {
location: {
ancestorOrigins: ['https://cdn.ampproject.org'],
},
document: {
referrer: 'https://cdn.ampproject.org/c/www.foo.com/test',
},
};
function invalid(fn) {
expect(fn).to.throw(/Invalid embedding hostname/);
}
validateAllowedEmbeddingOrigins(win, ['foo.com']);
validateAllowedEmbeddingOrigins(win, ['www.foo.com']);
invalid(() => validateAllowedEmbeddingOrigins(win, ['bar.com']));
invalid(() => validateAllowedEmbeddingOrigins(win, ['amp.www.foo.com']));
invalid(() => validateAllowedEmbeddingOrigins(win, ['ampwww.foo.com']));
win.document.referrer = 'https://cdn.ampproject.net/c/www.foo.com/test';
invalid(() => validateAllowedEmbeddingOrigins(win, ['foo.com']));
});

it('should validateAllowedEmbeddingOrigins: referrer non-cache', () => {
const win = {
location: {
},
document: {
referrer: 'https://www.foo.com/test',
},
};
function invalid(fn) {
expect(fn).to.throw(/Invalid embedding hostname/);
}
validateAllowedEmbeddingOrigins(win, ['foo.com']);
validateAllowedEmbeddingOrigins(win, ['www.foo.com']);
invalid(() => validateAllowedEmbeddingOrigins(win, ['bar.com']));
invalid(() => validateAllowedEmbeddingOrigins(win, ['amp.www.foo.com']));
invalid(() => validateAllowedEmbeddingOrigins(win, ['ampwww.foo.com']));
});

it('should validateAllowedEmbeddingOrigins: referrer cache', () => {
const win = {
location: {
},
document: {
referrer: 'https://cdn.ampproject.org/c/www.foo.com/test',
},
};
function invalid(fn) {
expect(fn).to.throw(/Invalid embedding hostname/);
}
validateAllowedEmbeddingOrigins(win, ['foo.com']);
validateAllowedEmbeddingOrigins(win, ['www.foo.com']);
invalid(() => validateAllowedEmbeddingOrigins(win, ['bar.com']));
invalid(() => validateAllowedEmbeddingOrigins(win, ['amp.www.foo.com']));
invalid(() => validateAllowedEmbeddingOrigins(win, ['ampwww.foo.com']));
win.document.referrer = 'https://cdn.ampproject.net/c/www.foo.com/test';
invalid(() => validateAllowedEmbeddingOrigins(win, ['foo.com']));
});
});

0 comments on commit e5b705c

Please sign in to comment.