Cross domain iframe mic permissions changed in Safari 12.1? - javascript

Safari 12.1 seems to have stopped remembering getUserMedia (microphone in this case) permissions for cross domain iframes.
Given a simplified example of two sites:
Domain A:
<!DOCTYPE html>
<html>
<head></head>
<body>
<button onclick="triggerUserMedia()">Get User Media</button>
<script>
function triggerUserMedia(){
const constraints = { audio: true, video: false };
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
console.log('got stream');
})
.catch(function(err) {
console.log('couldn\'t get the stream');
});
}
</script>
</body>
</html>
Domain B:
<!DOCTYPE html>
<html style="height:100%">
<head></head>
<body>
<iframe src="https://domain-a/index.html" allow="microphone"></iframe>
</body>
</html>
In Safari pre 12.1, and in the current build of Chrome etc., pressing the button multiple times when embedded in Domain B would result in one single "Allow “Domain A” to use your microphone?", just on the first press.
However, it now results in a permission popup every time the button is pressed.
Does anyone know what specifically changed in Safari 12.1 which is caused this behaviour to change? (Is this a new webkit security restriction?)
What can be done to ensure the permission is only asked for once, as it was before?
(We call getUserMedia multiple times in a project intended to be embedded in different sites, so this is causing a significant user experience impact)

I can't answer as to what specifically is going on with browser vendors making iframe getUserMedia requests reprompt for permission every time a media stream is requested, but we found the solution to be to use the parents mediaDevices singleton to request for a stream.
function getRootWindow(window) {
if (window.parent === window) {
return window;
}
return getRootWindow(window.parent);
}
getRootWindow(window).navigator.mediaDevices.getUserMedia().then(...);
We have tested this on iOS, FF, Chrome, Safari and it works fine, once permissions have been granted subsequent requests for a media device work fine. Something about calling this from the iFrames DOM causes it to issue prompts every request.

I'm seeing the same issue even for the same domain iframe - simply asking for getUserMedia from inside iframe always asks for permission.
I figured out an ugly workaround even for cross-domain: if you at least once ask for permission on "host" page, it'll start working inside iframe. It requires you to have some "host page" script that'll communicate with iframe (via window messages to avoid cross-domain issues) but it works.
Another weird behavior even using my workaround is that enumerateDevices do not return valid device names if you first ask a permission on host page and then inside iframe. So a full workaround (if you need enumeration) would be to have 2 permission prompts:
first ask inside iframe (this will make sure enumeration works)
and then on host page (this will ensure Safari will remember permission and won't show prompt for next getUserMedia invokes)

Related

FB.login() fails with "Unsafe JavaScript attempt to initiate navigation for frame" on Android Chrome but not desktop Chrome

I have a Facebook JS SDK login flow here: https://web.triller.co/#/user/login
When the user taps the Facebook button, the following function is executed:
loginFacebook()
{
const fbPromise = new Promise((resolve, reject) => {
FB.login(resp => {
if (resp.authResponse)
{
resolve(resp.authResponse.accessToken);
}
else
{
console.log(resp);
reject(new Error('Facebook login canceled or failed.'));
}
});
});
return fbPromise
.then(accessToken => api.postJson('user/login_facebook', { accessToken }))
.then(this._handleLogin.bind(this));
}
Basically, it calls FB.login(), and expects to receive a valid resp.authResponse. Unfortunately, it doesn't, even after successfully authenticating on the Facebook popup/tab. Instead, we receive { authResponse: undefined, status: undefined } and the following error from the browser:
Unsafe JavaScript attempt to initiate navigation for frame with URL
'https://m.facebook.com/v2.8/dialog/oauth?foo=bar'
from frame with URL 'https://web.triller.co/#/user/login?_k=cmzdb6'.
The frame attempting navigation is neither same-origin with the
target, nor is it the target's parent or opener.
The error occurs immediately after authenticating within the Facebook popup/tab, and it only occurs on Android Chrome. Desktop Chrome (on a Mac) does not show the same error. Safari on iOS does not show the error, either.
Any thoughts on what's going on? Why the difference between Android Chrome and desktop Chrome? Could it have something to do with the hash in the URL?
In desktop this issue can be caused by XFINITY Constant Guard Protection Suite Chrome extension. Disble it and problem will be solved.
In Android, try removing XFINITY or similar security extensions or applications (Norton security antivirus).
https://developer.salesforce.com/forums/?id=906F00000008qKnIAI
I think it fails because of the m.facebook.com/... based on this https://en.wikipedia.org/wiki/Same-origin_policy#Origin_determination_rules (see the case for http://en.example.com/dir/other.html and why it fails). Although it shouldn't based on what I've done with the fb api this is weird, try making a cors request instead as a workaround?
This is a known issue here https://code.google.com/p/android/issues/detail?id=20254.
When the window.open is called without proper arguments, it will not be opened as expected.
It can be overcome by either updating the browser (for client, check the browser version and if old one, force them to update it).
Otherwise (not applicable for you since you can't change the FB function), pass the correct arguments to window.open
Interestingly I only seem to get this when I am using the mobile device simulator in Chrome.
Looking at the source code for the Facebook SDK there is the following line:
url: i.resolve(a.display == "touch" ? "m" : "www") + "/" + d.url,
Now "m" then becomes "m.facebook.com" whereas "www" becomes "www.facebook.com". So it's using a different URL based on whether or not it's touch.
Now they're the same domain anyway, and the connect API is loaded from facebook.net (not .com) so it's not as simple as just being a different domain. However I suspect the issue could be related to this.
In either case when not in mobile device testing mode I don't get the error.

Why is "window.top.location" working in iframes?

If I can insert iframes in a forum comment, example:
<iframe src="http://badwebsite.comm/xss.html"></iframe>
cat xss.html
<html>
<head>
<script>
window.top.location = "http://badwebsite.comm/stolecred.html";
</script>
</head>
</html>
So when anyone enters this forum, it will be redirected to http://badwebsite.comm/stolecred.html/.
From this point, the stolecred.html can be an exact copy of the login page for the original forum.
After the user gave his credentials, the http://badwebsite.comm/stolecred.html can redirect it to the original website.
Question:
So allowing iframes in a forum is a very big security problem? It shouldn't be allowed in any manner? Why does a modern browser allow window.top.location to work in an iframe?
There is no limited risk (as long as there isn't a bug in the browser).
If you run the example below, you'll get the following error message in the JavaScript console of your browser:
Unsafe JavaScript attempt to initiate navigation for frame with URL 'Why is "window.top.location" working in iframes?' from frame with URL 'http://schneidr.de/misc/bad_redirect.html'. The frame attempting navigation of the top-level window is sandboxed, but the 'allow-top-navigation' flag is not set.
(Error message from Chrome, wording may be different in other browsers)
<iframe src="http://schneidr.de/misc/bad_redirect.html"></iframe>
The reason for the error is, that the browser doesn't trust JavaScript that is embedded via a frame from a different domain. Because of this it is automatically sandboxed.
To allow the JavaScript changes in an iframe you need to set the sandbox="" attribute, for this example to sandbox="allow-top-navigation". So, if you allow iframes in comments you should at least filter this attribute before saving and displaying it.
If you do this it is pretty safe to allow iframes from a security point of view. Personally I wouldn't allow it on a site I administer because I have no control over the content displayed via the iframe, which could bring me legal trouble.

Basic Google Sign-In for Websites code not working in Internet Explorer 11

I am attempting to use Google Sign-In for Websites (https://developers.google.com/identity/sign-in/web/) and noticed that my solution is not working in Internet Explorer 11. To try to eliminate as many factors as possible, I created a simple test case based on the sample code provided by Google.
I've tested it in Chrome on my Windows 7 PC, Chrome on my Mac, Safari on my Mac, Firefox on my Mac and Safari on my iPhone. It works on all of these (e.g., when I click the sign in button and select/enter my Google account, it returns to the page and the button says, "Signed in").
It does not, however, work on Internet Explorer 11 on PC or, strangely enough, Chrome for iOS. When the button is clicked, a window opens to allow me to select my Google account, but after making a selection, the window closes and returns to the page with a button that still says, "Sign In."
Here is the sample code:
<html>
<head>
<meta name="google-signin-client_id" content="61023618497-vqfbod57f26ncjl9d6firk3t09ve4tt3.apps.googleusercontent.com">
<script src="https://apis.google.com/js/platform.js" async defer></script>
</head>
<body>
<div class="g-signin2"></div>
</body>
</html>
Any ideas as to what might be going on? I've searched around and have not found any solutions.
One idea was to add "accounts.google.com" to IE's Trusted Sites. This didn't work. I also tried accessing the page via https instead of http. That didn't make a difference either. Anything else I should try?
I ran into the same problem now, a few months later.
If you look at:
https://developers.google.com/identity/sign-in/web/build-button
and try a signin with their demo button, it does not work with IE11 (but it works with other browsers I am using at different OS). I could not find any solution.
I am leaving the comment for a future reader, who searches for the same problem in the future. If the demo button at Google does not work, she can at least rest assured that the problem is probably not in her code. :)
Be sure that you don't have IE 11 configured to block all third party cookies.
Third party cookies are required, and the user experience that occurs when third party cookies are blocked--as you've discovered--leaves much to be desired. There is no warning or error message presented to the user.
You could try to catch the error before it happens. It is possible to detect whether or not third party cookies are blocked by trying to set a cookie on a second domain (that you control) and then making a second request to ensure the cookie is set. You'll need a script or something on your server that can set and check for the cookie (it can't be done using only JavaScript because of the browser security model).
I've had success doing the following:
Loading the script to initiate the Google button rendering etc. from $document.ready. (i.e.Whatever you have in the apis.google.com/js/client:platform.js?onload= x )
e.g.
<script src="https://apis.google.com/js/client:platform.js?onload=startApp" async defer></script>
Move startApp() to here:
$(document).ready(function () {
startApp();
Where startApp() looks something like this:
function startApp() {
gapi.load('auth2', function () {
gapi.client.load('plus', 'v1').then(function () {
gapi.signin2.render('signin-button', {
scope: 'https://www.googleapis.com/auth/plus.login',
fetch_basic_profile: false
});
gapi.auth2.init({
fetch_basic_profile: false,
scope: 'https://www.googleapis.com/auth/plus.login'
}).then(
function () {
console.log('init');
auth2 = gapi.auth2.getAuthInstance();
auth2.isSignedIn.listen(updateSignIn);
auth2.then(updateSignIn());
});
});
});
}
I struggled to get the example to work on localhost, but as soon as I deployed it to the real URL, it worked.

Dom Exception 18 in javascript/html5 when trying to access local storage

I have the following html that tries to set one key in local storage.
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<script>
document.addEventListener('DOMContentLoaded', loaded, false);
function loaded(){
try {
window.localStorage.setItem("Test", "SetItemValue");
document.getElementById("test").innerHTML = "Test OK";
} catch (err) {
document.getElementById("test").innerHTML = "Test FAIL<br>" + err.message;
}
}
</script>
</head>
<body>
<div id="test">Testing...</div>
</body>
</html>
On one single iPhone5 this causes the following exception.
Test FAIL SecurityError: DOM Exception 18
Other iPhones tested (three others) with the same iOs-version (7.0.2) works.
I have tested the above page from both a https://x.y.domain.tld and a http://x.domain.tld with the same exception.
Other questions concerning "DOM Exception 18" seem to be about security settings when eg. testing on localhost but linking in remote content over https. But this is a simple html page that simply tries to access local storage.
I read somewhere that if cookies are blocked, the DOM Exception 18 error shows up when setting localStorage. I was able to reproduce the error (not sure if I reproduced the issue, per se) on a simulator iPhone 5 (w/ iOS7) by going to the Settings, then for Safari, "Block Cookies" always. Don't know if that's how your iPhone 5 is configured though...
Problem was solved. It was revealed that the client (the errant phone was a client phone) uses a company-wide security platform installed on their iPhones. That platform has a separate web browser that must be used to enable every Javascript feature. The end user with the phone didn't know this so he used Safari that somehow is crippled when this platform is active. So the solution was to use the right application for browsing.
The platform was http://www.mobileiron.com/ and the secure browser is called Web#Work
I've seen that you can't change data in local storage within the first couple of seconds. Set a timer for 5000 milliseconds, and then run the setItem-function to see what happens.

Will a browser give an iframe a separate thread for JavaScript?

Do web browsers use separate executional threads for JavaScript in iframes?
I believe Chrome uses separate threads for each tab, so I am guessing that JavaScript in an iframe would share the same thread as its parent window, however, that seems like a security risk too.
Recently tested if JavaScript running in a iFrame would block JavaScript from running in the parent window.
iFrame on same domain as parent:
Chrome 68.0.3440.84: Blocks
Safari 11.0.2 (13604.4.7.1.3): Blocks
Safari 15.1 on iOS: Blocks
Firefox 96: Blocks
iFrame on different domain as parent
Chrome 68.0.3440.84: Doesn't block
Safari 11.0.2 (13604.4.7.1.3): Blocks (outdated, but I don't have a macbook)
Safari 15.1 on iOS: Doesn't block
Firefox 96: Doesn't block
Chrome for Android 96: sometimes Blocks and sometimes Doesn't block (There are some complex rules in Chrome for Android that determine when Chrome for Android does and doesn't isolate a process, see chrome://process-internals and chrome://flags)
parent.html:
<body>
<div id="count"></div>
<iframe src="./spin.html"></iframe>
<script>
let i = 0;
let div = document.getElementById("count");
setInterval(() => {
div.innerText = i++;
}, 100);
</script>
</body>
spin.html:
<body>
<button id="spin">spin</button>
<script>
const spin = document.getElementById("spin");
spin.addEventListener('click', () => {
const start = Date.now();
while (Date.now() - start < 1000) { }
})
</script>
</body>
Before chrome came along, all tabs of any browser shared the same single thread of JavaScript. Chrome upped the game here, and some others have since followed suit.
This is a browser implementation detail, so there is no solid answer. Older browsers definitely don't. I don't know of any browser that definitely uses another thread for iframes, but to be honest I've never really looked into it.
It isn't a security risk, as no objects are brought along with the thread execution.
To sum up the other answers: No, iFrames usually run in the same thread/process as the main page.
However, it appears the Chromium team are working on further isolation in this area:
Chromium Issue 99379: Out of process iframes [sorry, link not working - if you can find a link to the issue that works, please let me know]
Design Plans for Out-of-Process iframes
I've had the same question myself this night, before checking for any existing answers. In the project I'm currently working we have to load an iFrame that uses a different framework and I was curios if that iFrame could somehow block the thread and affect my app. The answer is yes, it can.
My test was done in Chrome. In the parent I've loaded a child iFrame. In the parent I've set an interval to console.log a text every amount time. Then in the iFrame I've used a timeout to launch a 'while' that blocks the thread. The answer: the iFrame uses the same thread.
Example:
In the parent:
setInterval(() => {
console.log('iFrame still using the thread');
}, 3000)
In the iFrame:
setTimeout(() => {
console.log('now the thread is not working in the iFrame anymore');
while (true) {
}
}, 10000)
2021 Update:
There is now the Origin-Agent-Cluster header which allows you to request dedicated resources for an iframe. It is currently supported on Chrome (88+) with positive reception from Mozilla and Safari.
Origin-Agent-Cluster is a new HTTP response header that instructs the browser to prevent synchronous scripting access between same-site cross-origin pages. Browsers may also use Origin-Agent-Cluster as a hint that your origin should get its own, separate resources, such as a dedicated process.
[...] For example, if https://customerservicewidget.example.com expects to use lots of resources for video chat, and will be embedded on various origins throughout https://*.example.com, the team maintaining that widget could use the Origin-Agent-Cluster header to try to decrease their performance impact on embedders.
To use the Origin-Agent-Cluster header, configure your web server to send the following HTTP response header: Origin-Agent-Cluster: ?1 The value of ?1 is the structured header syntax for a boolean true value.
More details here: https://web.dev/origin-agent-cluster/
Only chrome & firefox on desktop (no, not mobile) is separating threads.
I've created a small page that run long loop in interval in the main page, and shows an animation both in the main page and in the iframe.
You can go to the site from the browser you wish to check.
If the lower animation (under 'crossorigin') runs without stopping, it's have a separate thread.
https://eylonsu.github.io/browser_thread/
Late on this but... good point, cause iframe js seems to be concurrent in Firefox 16.
Try with alert function (blocking), you'll see dialogs opening together.
You won't see that in Chrome or IE.
iframe js may access the parent window in Firefox 16 as usual, so I can think of possible race conditions arising.
Did some experimenting with this today in Chrome 28 in Ubuntu. Used this command to see Chrome's threads and processes
ps axo pid,nlwp,cmd | grep "chrome"
It looks like Chrome does not spawn new threads or processes for iframes. An interesting note is that it does spawn a new process for the dev tools pane.
2022 Update (Experimental)
Iframes can now be run in parallel in at least Chrome Canary on desktop computers, but this is still experimental.
Download Chrome Canary (https://www.google.com/chrome/canary/).
Navigate to "chrome://flags/".
Enable "Isolated sandboxed iframes".
Create "index.html" with the following content:
<h1>index.html</h1>
<iframe src="index-child.html" sandbox="allow-scripts"></iframe>
<script>
setInterval(() => {
console.log("index.html executed one iteration");
}, 1000)
</script>
Create "index-child.html" with the following content:
<h1>index-child.html</h1>
<script>
setTimeout(() => {
console.log("index-child.html started continuous execution");
while (true) {
}
}, 3000)
</script>
Open "index.html" in the browser.
Verify that the console is consistently logging "index.html executed one iteration". Thus, the iframe is executed in parallel.
Disable "Isolated sandboxed iframes" (or just use another browser) and open "index.html" again. The console is no longer consistently logging "index.html executed one iteration". Thus, the iframe is no longer executed in parallel.
Note: The sandbox attribute on the iframe tag must be correctly set for this to work. Additionally, only one extra process per site is currently supported, which means that multiple iframes will not all run in parallel.
The specific instructions from "chrome://flags/":
Isolated sandboxed iframes
When enabled, applies process isolation to iframes with the 'sandbox' attribute and without the 'allow-same-origin' permission set on that attribute. The current isolation model is that all sandboxed iframes from a given site will be placed into the same process, but alternative models may be introduced in future experiments. – Mac, Windows, Linux, Chrome OS, Fuchsia
For iFrames, no. However if you want to use threads in JavaScript you can use Web Workers, a working html5 draft supported by the new browsers. http://www.w3.org/TR/2009/WD-workers-20091029/

Categories

Resources