La politique âSame Originâ (même site) limite lâaccès des fenêtres et des iframe les uns aux autres.
Lâidée est que si un utilisateur a deux pages ouvertes : une de john-smith.com, et une autre de gmail.com, alors il ne voudra pas dâun script de john-smith.com pour lire son courrier de gmail.com. Lâobjectif de la politique âSame originâ est donc de protéger les utilisateurs contre le vol dâinformations.
Same Origin
Deux URLs sont dites de âSame originâ si elles ont le mêmes protocole, domaine et port.
Ces URLs partagent toutes la même origine (âSame originâ):
http://site.comhttp://site.com/http://site.com/my/page.html
Ce nâest pas le cas de celles ci:
http://www.site.com(différent domaine:www.importe)http://site.org(différent domaine:.orgimporte)https://site.com(différent protocole:https)http://site.com:8080(différent port:8080)
La politique âSame Originâ indique que:
- si nous avons une référence à une autre fenêtre, par exemple un popup créé par
window.openou une fenêtre à lâintérieur de<iframe>, et que cette fenêtre provient de la même origine, alors nous avons un accès complet à cette fenêtre. - sinon, sâil provient dâune autre origine, alors nous ne pouvons pas accéder au contenu de cette fenêtre : variables, document, quoi que ce soit. La seule exception est la
location:nous pouvons la modifier (et donc rediriger lâutilisateur). Mais nous ne pouvons pas lire la localisation (donc nous ne pouvons pas voir où se trouve lâutilisateur, pas de fuite dâinformation).
En action: iframe
Une balise <iframe> héberge une fenêtre intégrée séparée, avec ses propres objets document et window séparés.
Nous pouvons y accèder en utilisant ces propriétés:
iframe.contentWindowpour accèder à la fenêtre à lâintérieur de<iframe>.iframe.contentDocumentpour accèder au document à lâintérieur de<iframe>, abréviation deiframe.contentWindow.document.
Lorsque nous accédons à quelque chose à lâintérieur de la fenêtre intégrée, le navigateur vérifie si lâiframe a la même origine. Si ce nâest pas le cas, lâaccès est refusé (location est une exception, câest toujours autorisé).
Par exemple, essayons de lire et dâécrire dans un <iframe> dâune autre origine :
<iframe src="https://example.com" id="iframe"></iframe>
<script>
iframe.onload = function() {
// nous pouvons obtenir la référence de la fenêtre intérieure
let iframeWindow = iframe.contentWindow; // OK
try {
// ...mais pas du document à l'intérieur
let doc = iframe.contentDocument; // ERREUR
} catch(e) {
alert(e); // Erreur de sécurité (origine différente)
}
// Nous ne pouvons pas non plus LIRE l'URL de la page dans l'iframe
try {
// impossible de lire l'URL de l'objet Location
let href = iframe.contentWindow.location.href; // ERREUR
} catch(e) {
alert(e); // Erreur de sécurité
}
// ...nous pouvons ECRIRE dans l'objet Location (et ainsi charger quelque chose d'autre dans l'iframe)!
iframe.contentWindow.location = '/'; // OK
iframe.onload = null; // libère le gestionnaire, ne pas l'exécuter après le changement de Location
};
</script>
Le code ci-dessus indique les erreurs pour toutes les opérations sauf:
- Obtenir la référence de la fenêtre intérieure
iframe.contentWindowâ câest autorisé. - Ecrire dans
location.
Au contraire, si <iframe> possède la même origine, nous pouvons tout faire avec:
<!-- iframe venant du même site -->
<iframe src="/" id="iframe"></iframe>
<script>
iframe.onload = function() {
// faites tout ce que vous voulez
iframe.contentDocument.body.prepend("Hello, world!");
};
</script>
Lâévènement iframe.onload (dans la balise <iframe>) est essentiellement la même chose que iframe.contentWindow.onload (sur lâobjet fenêtre intégré). Il se déclenche lorsque la fenêtre intégrée se charge complètement avec toutes les ressources.
â¦Mais nous ne pouvons pas accéder à iframe.contentWindow.onload pour une iframe provenant dâune autre origine, donc il faut utiliser iframe.onload.
Fenêtre en sous-domaine: document.domain
Par définition, deux URLs avec des domaines différents ont des origines différentes.
Mais si des fenêtres partagent le même domaine de second niveau, par exemple john.site.com, peter.site.com et site.com (de sorte que leur domaine de second niveau commun est site.com), nous pouvons faire en sorte que le navigateur ignore cette différence, afin quâelles puissent être considérées comme provenant de la âmême origineâ pour utiliser la communication entre fenêtres.
Pour que cela fonctionne, chacune de ces fenêtres doit exécuter le code:
document.domain = 'site.com';
Câest tout. Ils peuvent maintenant interagir sans limites. Encore une fois, cela nâest possible que pour les pages ayant le même domaine de second niveau.
La propriété document.domain est en cours de suppression de la spécification. La messagerie inter-fenêtres (expliquée ci-dessous) est le remplacement suggéré.
Cela dit, actuellement tous les navigateurs le supportent. Et le support sera conservé pour lâavenir, pour ne pas casser lâancien code qui repose sur document.domain.
Iframe: le piège du mauvais document
Lorsquâune iframe provient de la même origine, et que nous pouvons accéder à son document, il y a un piège. Ce nâest pas lié à des questions dâorigine croisée, mais il est important de le savoir.
Dès sa création, une iframe dispose immédiatement dâun document. Mais ce document est différent de celui qui sây charge !
Donc, si nous faisons quelque chose avec le document immédiatement, il sera probablement perdu.
Voici par exemple:
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
iframe.onload = function() {
let newDoc = iframe.contentDocument;
// le document chargé est différent du document initial !
alert(oldDoc == newDoc); // false
};
</script>
Nous ne devrions pas travailler avec le document dâune iframe qui nâa pas finis de charger, car câest le mauvais document. Si nous y plaçons des gestionnaires dâévénements, ils seront ignorés.
Comment détecter le moment où le bon document est là ?
Le bon document est définitivement présent quand iframe.onload se déclenche. Mais il ne se déclenche que lorsque toute lâiframe avec toutes les ressources est chargée.
Nous pouvons essayer de saisir le moment plus tôt en utilisant setInterval:
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
// toutes les 100ms on vérifie que le document est le nouveau
let timer = setInterval(() => {
let newDoc = iframe.contentDocument;
if (newDoc == oldDoc) return;
alert("New document is here!");
clearInterval(timer); // annule setInterval, nous n'en n'avons plus besoin
}, 100);
</script>
Collection: window.frames
Une autre façon dâobtenir lâobjet fenêtre (window) de <iframe> â est de lâobtenir à partir de la collection nommée window.frames :
- Par nombre:
window.frames[0]â lâobjet fenêtre pour la première frame du document. - Par nom:
window.frames.iframeNameâ lâobjet fenêtre pour la frame avecname="iframeName".
Par exemple:
<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>
<script>
alert(iframe.contentWindow == frames[0]); // true
alert(iframe.contentWindow == frames.win); // true
</script>
Une iframe peut contenir dâautres iframes. Les objets window correspondants forment une hiérarchie.
Les liens de navigation sont:
window.framesâ la collection de fenêtres âenfantsâ (pour les cadres emboîtés).window.parentâ la référence à la fenêtre âparentâ (extérieure).window.topâ la référence à la fenêtre parentale la plus élevée.
Par exemple:
window.frames[0].parent === window; // true
Nous pouvons utiliser la propriété top pour vérifier si le document actuel est ouvert à lâintérieur dâun cadre ou non:
if (window == top) { // fenêtre actuelle == window.top?
alert('The script is in the topmost window, not in a frame');
} else {
alert('The script runs in a frame!');
}
Lâattribut iframe âsandboxâ
Lâattribut sandbox permet dâexclure certaines actions à lâintérieur dâune <iframe> afin dâempêcher lâexécution de code non fiable. Il âsandboxeâ lâiframe en la traitant comme provenant dâune autre origine et/ou en lui appliquant dâautres limitations.
Il y a un âensemble par défautâ de restrictions appliquées pour <iframe sandbox src="...">. Mais il peut être assoupli si nous fournissons une liste de restrictions séparées par des espaces qui ne doivent pas être appliquées comme valeur de lâattribut, comme ceci : <iframe sandbox="allow-forms allow-popups">.
En dâautres termes, un attribut "sandbox" vide pose les limites les plus strictes possibles, mais nous pouvons mettre une liste délimitée par des espaces de celles que nous voulons lever.
Voici la liste des limitations:
allow-same-origin- Par défaut
"sandbox"impose la politique des âorigines différentesâ pour lâiframe. En dâautres termes, elle oblige le navigateur à traiter lâiframecomme provenant dâune autre origine, même si sasrcpointe vers le même site. Avec toutes les restrictions implicites pour les scripts. Cette option supprime cette fonctionnalité. allow-top-navigation- Permet à lâ
iframede changerparent.location. allow-forms- Permet de soumettre des formulaires à partir de lâ
iframe. allow-scripts- Permet dâexécuter des scripts à partir de lâ
iframe. allow-popups- Permet de
window.opendes pop-up depuis lâiframe
Voir le manuel pour plus de détails.
Lâexemple ci-dessous montre une iframe en âsandboxâ avec lâensemble des restrictions par défaut : <iframe sandbox src="...">. Il contient du JavaScript et un formulaire.
Nous pouvons noter que rien ne fonctionne. Le réglage par défaut est donc très sévère :
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div>The iframe below has the <code>sandbox</code> attribute.</div>
<iframe sandbox src="sandboxed.html" style="height:60px;width:90%"></iframe>
</body>
</html><!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<button onclick="alert(123)">Click to run a script (doesn't work)</button>
<form action="http://google.com">
<input type="text">
<input type="submit" value="Submit (doesn't work)">
</form>
</body>
</html>Le but de lâattribut "sandbox" est uniquement dâajouter des restrictions. Il ne peut pas les supprimer. En particulier, il ne peut pas assouplir les restrictions de même origine si lâiframe provient dâune autre origine.
Messagerie entre fenêtres
Lâinterface postMessage permet aux fenêtres de se parler, quelle que soit leur origine.
Câest donc un moyen de contourner la politique de âSame Originâ Elle permet à une fenêtre de john-smith.com de parler à gmail.com et dâéchanger des informations, mais seulement si les deux parties sont dâaccord et appellent les fonctions JavaScript correspondantes. Cela rend le système sûr pour les utilisateurs.
Lâinterface comporte deux parties.
postMessage
La fenêtre qui veut envoyer un message appelle la méthode postMessage de la fenêtre de réception. En dâautres termes, si nous voulons envoyer le message à win, nous devons appeler win.postMessage(data, targetOrigin).
Arguments:
data- Les données à envoyer. Peut être nâimporte quel objet, les données sont clonées à lâaide de lââalgorithme de clonage structuréâ. IE ne supporte que les chaînes de caractères, nous devrions donc
JSON.stringifydes objets complexes pour ce navigateur. targetOrigin- Spécifie lâorigine de la fenêtre cible, de sorte que seule une fenêtre de lâorigine donnée recevra le message.
Le targetOrigin est une mesure de sécurité. Rappelez-vous que si la fenêtre cible provient dâune autre origine, nous ne pouvons pas lire sa location dans la fenêtre de lâexpéditeur. Nous ne pouvons donc pas savoir quel site est ouvert dans la fenêtre cible à lâheure actuelle : lâutilisateur pourrait naviguer ailleurs, et la fenêtre émettrice nâen aurais aucune idée.
En spécifiant targetOrigin , on sâassure que la fenêtre ne reçoit les données que si elle se trouve toujours au bon endroit. Câest important lorsque les données sont sensibles.
Par exemple, ici win ne recevra le message que sâil possède un document de lâorigine http://example.com :
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("message", "http://example.com");
</script>
Si nous ne voulons pas de ce contrôle, nous pouvons régler targetOrigin sur *.
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("message", "*");
</script>
onmessage
Pour recevoir un message, la fenêtre cible doit avoir un gestionnaire sur lâévénement message. Il se déclenche lorsque postMessage est appelé (et que la vérification targetOrigin est réussie).
Lâobjet de lâévénement a des propriétés particulières :
data- Les données de
postMessage. origin- Lâorigine de lâexpéditeur, par exemple
http://javascript.info. source- La référence à la fenêtre de lâexpéditeur. Nous pouvons immédiatement faire revenir
source.postMessage(...)si nous le voulons.
Pour assigner ce gestionnaire, nous devrions utiliser addEventListener, la courte syntaxe window.onmessage ne fonctionne pas.
Voici un exemple:
window.addEventListener("message", function(event) {
if (event.origin != 'http://javascript.info') {
// quelque chose d'un domaine inconnu, ignorons-le
return;
}
alert( "received: " + event.data );
// peut envoyer un message en retour en utilisant event.source.postMessage(...)
});
Lâexemple complet:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
Receiving iframe.
<script>
window.addEventListener('message', function(event) {
alert(`Received ${event.data} from ${event.origin}`);
});
</script>
</body>
</html><!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form id="form">
<input type="text" placeholder="Enter message" name="message">
<input type="submit" value="Click to send">
</form>
<iframe src="iframe.html" id="iframe" style="display:block;height:60px"></iframe>
<script>
form.onsubmit = function() {
iframe.contentWindow.postMessage(this.message.value, '*');
return false;
};
</script>
</body>
</html>Résumé
Pour appeler des méthodes et accéder au contenu dâune autre fenêtre, nous devons dâabord en avoir une référence.
Pour les pop-ups, nous avons ces références:
- De la fenêtre ouvrante :
window.openâ ouvre une nouvelle fenêtre et renvoie une référence à celle-ci, - From the popup:
window.openerâ is a reference to the opener window from a popup.
Pour les iframes, nous pouvons accéder aux fenêtres parents/enfants en utilisant :
window.framesâ une collection dâobjets de fenêtres emboîtées,window.parent,window.topsont les références aux fenêtres parents et supérieures,iframe.contentWindowest la fenêtre à lâintérieur dâune balise<iframe>.
Si les fenêtres partagent la même origine (hôte, port, protocole), alors les fenêtres peuvent faire ce quâelles veulent les unes avec les autres.
Sinon, les seules actions possibles sont:
- Modifier la
locationdâune autre fenêtre (accès en écriture uniquement)⦠- Poster un message.
Les exceptions sont:
- Les fenêtres qui partagent le même domaine de second niveau :
a.site.cometb.site.com. Le fait de mettredocument.domain='site.com'dans les deux les met dans lâétat âsame originâ. - Si une iframe possède un attribut
sandboxelle est mise de force dans lâétat âdifferent originâ à moins que la valeur de lâattribut ne spécifieallow-same-origin. Cela peut être utilisé pour exécuter du code non fiable dans les iframes du même site.
Lâinterface postMessage permet à deux fenêtres, quelle que soit leur origine, de dialoguer:
-
Lâexpéditeur appelle
targetWin.postMessage(data, targetOrigin). -
Si
targetOriginnâest pas'*', alors le navigateur vérifie si la fenêtretargetWinpossède lâoriginetargetOrigin. -
Si câest le cas, alors
targetWindéclenche lâévénementmessageavec des propriétés spéciales:originâ lâorigine de la fenêtre de lâexpéditeur (commehttp://my.site.com)sourceâ la référence à la fenêtre de lâexpéditeur.dataâ les données, tout objet partout sauf dans IE qui ne supporte que des chaînes de caractères.
Nous devrions utiliser
addEventListenerpour définir le gestionnaire de cet événement à lâintérieur de la fenêtre cible.
Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâ¦)