This is a low severity bug, found by inspection, and confirmed with a real extension.
Chrome Apps and extensions with the "background" permission in their manifest are able to call window.open(url, "_blank", "background") to open a background window.
If the manifest additionally specifies "background.allow_js_access = false", then this operation is supposed to disallow the caller from getting a WindowProxy reference to the opened window.
However, the filtering we do is insufficient:
// If the opener is trying to create a background window but doesn't have
// the appropriate permission, fail the attempt.
if (container_type == content::mojom::WindowContainerType::BACKGROUND) {
#if BUILDFLAG(ENABLE_EXTENSIONS)
ProfileIOData* io_data = ProfileIOData::FromResourceContext(context);
InfoMap* map = io_data->GetExtensionInfoMap();
if (!map->SecurityOriginHasAPIPermission(source_origin,
opener_render_process_id,
APIPermission::kBackground)) {
return false;
}
// Note: this use of GetExtensionOrAppByURL is safe but imperfect. It may
// return a recently installed Extension even if this CanCreateWindow call
// was made by an old copy of the page in a normal web process. That's ok,
// because the permission check above would have caused an early return
// already. We must use the full URL to find hosted apps, though, and not
// just the origin.
const Extension* extension =
map->extensions().GetExtensionOrAppByURL(opener_url);
if (extension && !extensions::BackgroundInfo::AllowJSAccess(extension))
*no_javascript_access = true;
#endif
return true;
}
The problem arises because, in the above code, the security check is done using |source_origin|, but the no_js_access lookup is done using |opener_url|. But if opener_url is about:blank, can So, if we have an extension page with a manifest like:
{
"manifest_version": 2,
"minimum_chrome_version": "23",
"name": "Crosh Window extension (ncarter hacked)",
"update_url": "http://clients2.google.com/service/update2/crx",
"version": "1",
"permissions": ["background"],
"background": {
"allow_js_access": false
}
}
Then, inside that extension, the following operation doesn't work:
w = window.open("", "", "background") // is blocked
BUT, the following equivalent operation does work:
w = document.body.appendChild(document.createElement('iframe')).contentWindow.open("", "", "background") // opens a window
To fix this, we should only do one extension lookup and use it for both checks -- probably based on the SiteInstance SiteURL of the opener frame.
This is a low severity bug, found by inspection, and confirmed with a real extension.
Chrome Apps and extensions with the "background" permission in their manifest are able to call window.open(url, "_blank", "background") to open a background window.
If the manifest additionally specifies "background.allow_js_access = false", then this operation is supposed to disallow the caller from getting a WindowProxy reference to the opened window.
However, the filtering we do is insufficient:
// If the opener is trying to create a background window but doesn't have
// the appropriate permission, fail the attempt.
if (container_type == content::mojom::WindowContainerType::BACKGROUND) {
#if BUILDFLAG(ENABLE_EXTENSIONS)
ProfileIOData* io_data = ProfileIOData::FromResourceContext(context);
InfoMap* map = io_data->GetExtensionInfoMap();
if (!map->SecurityOriginHasAPIPermission(source_origin,
opener_render_process_id,
APIPermission::kBackground)) {
return false;
}
// Note: this use of GetExtensionOrAppByURL is safe but imperfect. It may
// return a recently installed Extension even if this CanCreateWindow call
// was made by an old copy of the page in a normal web process. That's ok,
// because the permission check above would have caused an early return
// already. We must use the full URL to find hosted apps, though, and not
// just the origin.
const Extension* extension =
map->extensions().GetExtensionOrAppByURL(opener_url);
if (extension && !extensions::BackgroundInfo::AllowJSAccess(extension))
*no_javascript_access = true;
#endif
return true;
}
The problem arises because, in the above code, the security check is done using |source_origin|, but the no_js_access lookup is done using |opener_url|. But if opener_url is about:blank, |extension| is null, even though we looked up a valid extension inside of SecurityOriginHasAPIPermission.
To repro this, you can create an empty extension like:
{
"manifest_version": 2,
"minimum_chrome_version": "23",
"name": "Crosh Window extension (ncarter hacked)",
"update_url": "http://clients2.google.com/service/update2/crx",
"version": "1",
"permissions": ["background"],
"background": {
"allow_js_access": false
}
}
Then, load a page from that that extension (maybe navigate to its manifest.json, and execute JS via devtools). In the JS snippet doesn't work:
w = window.open("", "", "background") // is blocked
BUT, the following equivalent operation does work:
w = document.body.appendChild(document.createElement('iframe')).contentWindow.open("", "", "background") // opens a window
To fix this, we should only do one extension lookup and use it for both checks -- probably based on the SiteInstance SiteURL of the opener frame.
This is a low severity bug, found by inspection, and confirmed with a real extension.
Chrome Apps and extensions with the "background" permission in their manifest are able to call window.open(url, "_blank", "background") to open a background window.
If the manifest additionally specifies "background.allow_js_access = false", then this operation is supposed to disallow the caller from getting a WindowProxy reference to the opened window.
However, the filtering we do is insufficient:
// If the opener is trying to create a background window but doesn't have
// the appropriate permission, fail the attempt.
if (container_type == content::mojom::WindowContainerType::BACKGROUND) {
#if BUILDFLAG(ENABLE_EXTENSIONS)
ProfileIOData* io_data = ProfileIOData::FromResourceContext(context);
InfoMap* map = io_data->GetExtensionInfoMap();
if (!map->SecurityOriginHasAPIPermission(source_origin,
opener_render_process_id,
APIPermission::kBackground)) {
return false;
}
// Note: this use of GetExtensionOrAppByURL is safe but imperfect. It may
// return a recently installed Extension even if this CanCreateWindow call
// was made by an old copy of the page in a normal web process. That's ok,
// because the permission check above would have caused an early return
// already. We must use the full URL to find hosted apps, though, and not
// just the origin.
const Extension* extension =
map->extensions().GetExtensionOrAppByURL(opener_url);
if (extension && !extensions::BackgroundInfo::AllowJSAccess(extension))
*no_javascript_access = true;
#endif
return true;
}
The problem arises because, in the above code, the security check is done using |source_origin|, but the no_js_access lookup is done using |opener_url|. But if opener_url is about:blank, |extension| is null, even though we looked up a valid extension inside of SecurityOriginHasAPIPermission.
To repro this, you can create an empty extension like:
{
"manifest_version": 2,
"minimum_chrome_version": "23",
"name": "Crosh Window extension (ncarter hacked)",
"update_url": "http://clients2.google.com/service/update2/crx",
"version": "1",
"permissions": ["background"],
"background": {
"allow_js_access": false
}
}
Then, load a page from that that extension (maybe navigate to its manifest.json, and execute JS via devtools). Executing the following JS snippet doesn't work:
w = window.open("", "", "background") // is blocked
BUT, the following equivalent operation does work:
w = document.body.appendChild(document.createElement('iframe')).contentWindow.open("", "", "background") // opens a window
To fix this, we should only do one extension lookup and use it for both checks -- probably based on the SiteInstance SiteURL of the opener frame.
Comment 1 by nick@chromium.org
, Apr 21 2017