La principale limitation que nous avons rencontré lorsque nous avons commencé à effectuer des requêtes Ajax est la contrainte de “Same Origin Policy” qui nous est imposée par les paramètres de sécurité des navigateurs : nos requêtes Ajax ne peuvent être effectuées que vers le domaine sur lequel notre site est déployé.
Typiquement, depuis “blog.pascal.martin.fr”, il n’est pas permis d’effectuer de requête Ajax vers “www.google.com“ : le navigateur l’interdit.
Cet article va nous permettre d’apprendre à contourner cette sécurité, en effectuant des requêtes Ajax sans passer par l’objet XmlHttpRequest, mais en écrivant dynamiquement des balises <script>.
Application d’exemple
Pour mes exemples au sein de cet article, je considérerai que nous souhaitons développer une mini-application dont le but est d’effectuer des multiplications :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>Démo Ajax serveur distant</title>
<script type="text/javascript" src="lib/prototype.js"></script>
<script type="text/javascript" src="ajax-distant.js"></script>
</head>
<body>
<form action="" id="multiplier" onsubmit="return false;">
<div>
<input type="text" maxlength="6" size="8" id="a" name="a" />
*
<input type="text" maxlength="6" size="8" id="b" name="b" />
<input type="button" name="do" id="do" value="=" onclick="multiply();" />
<input type="text" size="8" id="result" name="result" readonly="readonly" />
</div>
</form>
<div id="msg"> </div>
<script type="text/javascript">
// Appui sur "ENTER" dans le formulaire => lance la multiplication
Event.observe($('multiplier'), 'keydown', function (evt) {
if (evt.keyCode == 13) {
multiply();
}
});
</script>
</body>
</html>
Deux champs de saisie, un bouton pour lancer la multiplication, et un champ pour afficher le résultat :
Dans le principe, nul besoin d’effectuer une requête Ajax pour effectuer une multiplication : le code Javascript suivant ferait parfaitement l’affaire :
var multiply_noAjax = function () {
var a = parseFloat($F('a'));
var b = parseFloat($F('b'));
$('result').value = a*b;
}; // multiply_noAjax
Pour faciliter les écritures, et ne pas avoir à changer la partie HTML de notre application, nous passerons par un alias : pour chaque nouvelle version, nous affecterons une nouvelle fonction à la variable multiply, par laquelle l’application passera toujours.
En travaillant avec la version “sans Ajax”, donc :
var multiply = multiply_noAjax;
Autrement dit, appeler la fonction multiply revient en fait à appeler la fonction multiply_noAjax.
Un appel Ajax “normal”
Passons maintenant à une version Ajaxifiée de notre application de calcul de multiplications : les deux opérandes sont passées, via un appel Ajax, à une application côté serveur, qui, elle, effectue la multiplication, et retourne le résultat.
Le code Javascript n’a alors plus qu’à afficher ledit résultat dans le champ correspondant, sans avoir à effectuer le moindre calcul.
Pour simplifier les choses (en particulier, la compatibilité cross-navigateur), j’utiliserai le Framework javascript prototype, que j’ai déjà présenté au cours de plusieurs articles.
Les données de retour transiteront au format JSON.
Je vous conseille donc la lecture des articles suivants :
- Un premier appel Ajax avec Prototype.js : Ajax.Request
- et Utiliser JSON comme format d’échange de données, avec prototype.js
Voici le code de la fonction Javascript effectuant l’appel Ajax :
var multiply_ajaxBase = function (url) {
var a = parseFloat($F('a'));
var b = parseFloat($F('b'));
var myAjax = new Ajax.Request(
url,
{
method: 'get',
parameters: {
a: a,
b: b
},
onSuccess: function (xhr, jsonValue) {
if (jsonValue) {
$('result').value = jsonValue;
}
}, // onSuccess
onException: function (xhr, e) {
$('msg').innerHTML = e;
} // onException
});
}; // multiply_ajaxBase
Et voici comment elle est appelée, avec l’URL du service de calcul passée en paramètre.
var multiply_ajaxLocal = function () {
multiply_ajaxBase('./multiplier-server.php');
}; // multiply_ajaxLocal
Naturellement, comme indiqué précédemment, nous aliasons cette fonction sous le nom multiply :
var multiply = multiply_ajaxLocal;
Et côté serveur, rien de très compliqué : on récupère les deux valeurs passées en GET, et on renvoit le résultat, au sein d’une en-tête X-JSON, tel qu’attendu par prototype :
$a = floatval($_GET['a']);
$b = floatval($_GET['b']);
$result = $a * $b;
// prototype.js attend des données dans
// une en-tête nommée X-JSON :
header('X-JSON:' . json_encode($result));
Note : A vous de gérer les éventuels cas d’erreurs (du genre si l’utilisateur saisit n’importe quoi)
Note 2 : Nous pourrions aussi renvoyer les données sur la sortie standard, et utiliser la propriété responseText de l’instance d’objet XMLHttpRequest côté JavaScript… Ce que nous ferions certainement si nous n’utilisions pas le framework Prototype.
Echec en cross-domain
A présent, que se passe-t’il si nous essayons d’appeler un service serveur localisé sur un autre nom de domaine que celui où se trouve notre page d’application cliente ?
var multiply_ajaxDistantErreur = function () {
multiply_ajaxBase('http://distant.example.com/ajax-distant/multiplier-server.php');
} // multiply_ajaxDistantErreur
(Libre à vous d’adapter l’URL pour pointer vers un autre site ; le tout étant que l’URL comporte un nom de domaine autre que celui où est déployée l’application HTML+Javascript ; Au besoin, ajouter un alias dans le fichiers hosts de votre système, et un second VirtualHost Apache fonctionne parfaitement - c’est ce que j’ai fait pour écrire cet article, d’ailleurs ^^)
Et, encore une fois, avec l’alias qui va bien :
var multiply = multiply_ajaxDistantErreur;
Lorsque nous cliquons sur le bouton censé lancer le calcul, nous obtenons affichage d’un message d’erreur :
Error: Permission denied to call method XMLHttpRequest.open
C’est ce à quoi nous nous attendions : la contrainte de “Same Origin Policy” nous empéche de réaliser des appels Ajax vers un nom de domaine autre que celui où est déployée notre application.
Solution à base de balise <script>
La contrainte de SOP nous empêche, nous venons de le constater, d’effectuer des appels Ajax vers un autre nom de domaine… Du moins, en passant par l’objet XMLHttpRequest.
Heureusement (du moins, dans le cadre de ce que nous voulons faire), cette contrainte n’est pas appliquée à tous les chargements de données distants.
En particulier, elle ne s’applique pas aux chargements de scripts Javascript : rien ne vous empêche d’écrire des balises <script> incluant un fichier Javascript depuis un autre nom de domaine que celui où est déployée votre application.
Code Javascript générant une balise <script> dynamique
Maintenant, plutôt que d’effectuer un “vrai” appel Ajax, nous pourrions créer dynamiquement une balise <script> et l’injecter dans le DOM de notre page d’application… En ajouotant les paramètres qui nous intéressent dans l’URL indiquée en attribut “src” de cette balise.
Au moment où la balise <script> sera injectée dans le DOM, le navigateur effectuera un appel vers le serveur distant pour charger le code Javascript correspondant ; mais on peut tout à fait utiliser un script (PHP, pour nos exemples) pour générer du Javascript !
Donc, pour notre cas :
var multiply_scriptTag = function () {
var a = parseFloat($F('a'));
var b = parseFloat($F('b'));
if ($('requestMultiplier')) {
// Suppression de la balise si elle existait déjà
// (pour faire un brin de ménage dans le DOM)
$('requestMultiplier').remove();
}
// Création de la nouvelle balise :
var script = document.createElement('script');
script.src = 'http://distant.example.com/ajax-distant/multiplier-server-tag.php?';
script.src += 'func=resultFunction';
script.src += '&a=' + a;
script.src += '&b=' + b;
script.id = 'requestMultiplier';
script.type = 'text/javascript';
// Et injection dans le DOM :
document.body.appendChild(script);
}; // multiply_scriptTag
Que fait cette fonction ?
- S’il existe déjà une balise
<script>utilisée pour effectuer cet appel distant, elle sera supprimée (inutile d’en avoir 36, avec seulement la dernière qui serve encore à quelque chose) - Création de la nouvelle balise :
- Renseignement de l’URL du code Javascript à charger, en lui passant les paramètres qui nous intéressent, dont un nom de fonction à appeler une fois le calcul effectué,
- Renseignement de l’
idet dutypede la balise,
- Et ajout de la balise à la fin de la page HTML.
Pourquoi indiquer à la page distante quelle sera la fonction à appeler une fois le calcul effectué ?
Parce que le chargement du script distant se fait en dehors de notre contrôle : une fois la balise ajoutée au document, nous n’avons plus aucune maîtrise ; et, en particulier, nous ne savons pas quand le script aura fini de charger - et nous n’avons donc aucun moyen de récupérer le résultat du calcul !
Alors que si le programme serveur génère, en fin du code Javascript, un appel à cette fonction, en lui passant en paramètre le résultat du calcul, nous saurons que celui-ci s’est terminé, et quelle valeur a été retournée.
Fonction appelée une fois le script distant chargé
Voici donc la fonction qui devra être appelée une fois le calcul terminé - une fois le script distant chargé :
/**
* Appelée au retour de l'appel "Ajax"
*/
var resultFunction = function (jsonValue) {
if (jsonValue) {
$('result').value = jsonValue;
}
else {
$('msg').innerHTML = 'Erreur...';
}
}; // resultFunction
Dans le principe, on récupère la valeur passée en paramètre, en on l’affiche dans le champ de résultat.
Page distante effectuant le calcul
Et pour la page appelée via l’attribut src de la balise <script> :
<?php
// Le calcul à effectuer
$a = floatval($_GET['a']);
$b = floatval($_GET['b']);
$result = $a * $b;
// Et la fonction JS qui devrait être appelée au retour
$funcResult = $_GET['func'];
$jsonResult = json_encode($result);
echo "$funcResult($jsonResult);";
Le principe est composé de trois étapes :
- Récupération des paramètres,
- Calcul,
- Et écriture du code Javascript qui sera interprété par le navigateur
- En l’occurrence, appel à la fonction chargée de gérer la valeur retournée.
Échange HTTP
Lorsque la balise <script> est dynamiquement ajoutée à la page, une requête HTTP est effectuée vers l’URL précisée en attribut src de celle-ci.
Cette requête HTTP, dans notre cas, est la suivante :
GET /ajax-distant/multiplier-server-tag.php?func=resultFunction&a=2&b=10 HTTP/1.1 Host: distant.example.com User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9b5) Gecko/2008040603 Firefox/3.0b5 Accept: */* Accept-Language: fr-fr,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip,deflate Accept-Charset: UTF-8,* Keep-Alive: 300 Connection: keep-alive Referer: http://tests/ajax-distant/demo.html
Et une fois les calculs terminés, la réponse suivante est émise par le serveur :
HTTP/1.1 200 OK Date: Mon, 14 Apr 2008 08:40:13 GMT Server: Apache/2.2.4 (Ubuntu) DAV/2 SVN/1.4.4 mod_fastcgi/2.4.2 PHP/5.2.3-1ubuntu6.3 proxy_html/2.5 mod_perl/2.0.2 Perl/v5.8.8 X-Powered-By: PHP/5.2.3-1ubuntu6.3 Content-Length: 19 Keep-Alive: timeout=15, max=99 Connection: Keep-Alive Content-Type: text/html; charset=UTF-8 resultFunction(20);
Du côté de l’application cliente, tout ce qui est vu par le navigateur est une portion de code Javascript à exécuter :
resultFunction(20);
Et, considérant la définition de la fonction resultFunction, cela provoquera l’affichage du résultat de la multiplication !
Format de données pour les échanges
Pour une donnée simple comme un résultat de multiplication, la question du format de données à utiliser pour les échanges est peu importante…
… Mais lorsque vous travaillerez avec des cas concrets d’appels distants, “quel format utiliser ?” est une question que vous vous poserez certainement.
J’ai tendance, dans ce genre de situations, à privilégier le format JSON :
- Facile à générer côté serveur : il existe des bibliothèques pour la plupart des langages orientés Web
- Facile à interpréter côté navigateur (c’est du code Javascript valide)
-
C’est du code Javascript valide[1] : considérant que votre appel distant se fait via une balise
<script type="text/javascript">, il semble logique que le serveur génère… du Javascript !
Quelques points à noter
Avant de terminer, j’aimerais attirer votre attention sur un point : avec ce mécanisme, vous offrez au serveur distant la possibilité d’injecter n’importe quelle portion de code Javascript dans votre page !
Autrement dit, vous offrez au serveur distant la possibilité de faire à peu près ce qu’il veut sur la page de votre site, via une injection XSS… Que vous avez volontairement créé. En vrac :
- Vol de cookies des utilisateurs,
- Ajout de publicités lui rapportant de l’argent - et non à vous,
- Affichage de n’importe quoi sur votre page - vous décrédibilisant éventuellement,
- …
Un exemple simple pour illustrer mes propos ?
Que se passera-t’il si le code du serveur distant contient la portion de code PHP suivante :
echo "document.getElementsByTagName('body')[0].innerHTML = 'MECHANT';";
Qui entraînera la génération de cette portion de code Javascript :
document.getElementsByTagName('body')[0].innerHTML = 'MECHANT';
Effectivement, en une ligne, le serveur distant a démoli votre site ^^
N’effectuez donc ce genre d’appel que vers des serveurs en lesquels vous avez entièrement confiance !
Note
[1] Oui, ça fait deux fois que je le dis ; mais c’est important

Commentaires
excellent ! comment faire de l'Ajax sans l'objet XHR ... et en contournant ses restrictions. Je suis tombé par hasard sur votre post en cherchant s'il etait possible de contourner la limitation "cross-domain" d'AJAX. Votre solution est très astucieuse et très clairement expliquée. Merci !
Ce n'est pas vraiment "ma" solution ^^ mais il est vrai qu'on ne la trouve que trop peu souvent mentionnée en français - comme trop de points traitant de développement Web :-(
La rédaction et les exemples sont 100% "miens", par contre, donc merci encore plus ;-)
Bonjour,
Juste un petit post de remerciement - cet article m'a été bien utile dans le développement d'un projet.
L'article est clair et explicite, c'est fantastique :)
Bonne continuation
-- Arthur
Merci :-)
Bonne continuation à vous aussi !
J'ai recherché longtemps ... je l'avais perdu dans mes liens divers mais je viens de retrouver cet article : totalement d'accord avec les deux remarques ci-dessus ... J'en avais déjà connaissance - de la possibilité de se passer des requêtes du type "xhr" mais la plus grande qualité de votre article est de l'expliquer "simplement" ... Merci du temps passé à rendre clair une technique un peu complexe et bonne continuation surtout !!
Merci :-)
Lorsque l'on intègre googlemaps dans un site, je crois qu'on utilise la même méthode.
On insère un lien du type:
<script src="http://maps.google.com/maps?file=ap..." type="text/javascript"></script>
et on crée un objet avec en paramètre l'id d'un div.
Quand on zoom ou se déplace dans la carte, ça télécharge des images depuis le site de google.
Étonnant car je parviens à communiquer avec un script php d'un autre domaine en utilisant seulement l'objet XMLHttpRequest, sans passer par prototype...
Quelqu'un peut-il faire le test pour confirmer cela ?
Au temps pour moi...
Après avoir vérifié, c'est mon php qui exécute le script distant et non l'XMLHttpRequest...
Mon schéma est donc JS <=> PHP local <=> PHP distant
Slt,
Il y a un truc que je ne comprend pas ou qui n'est pas logique...
Comment est il possible et de prétendre qu'on peut voler tes cookies (toi qui est en train de me lire) uniquement en chargeant une page sur un serveur et en ajoutant du code JS ???
Seul celui qui l'injecte le récupère et non pas l'utilisateur X!
A moins que X modifie le serveur par FTP login/pass ...
Bonsoir,
A partir du moment où vous pouvez injecter du code Javascript dans une page (c'est le cas ici), vous avez accès aux cookies de la page dans laquelle vous avez réalisé votre injection. (via
document.cookie)A partir du moment où vous avez accès à ces cookies, il suffit d'un bout de script pour les envoyer à un serveur distant (le votre, par exemple), où ils peuvent être loggués.
Une fois les cookies loggués, rien de plus facile pour vous que d'ouvrir un navigateur, et de les re-créer dans celui-ci (une manière relativement simple et graphique de faire est en utilisant l'extension "Web Developper" de Firefox)...
=> Vous avez, dans votre navigateur, les cookies qu'avait au départ un autre utilisateur -- le visiteur de la page sur laquelle se trouvait l'injection XSS... Et bien souvent, cela signifie que vous avez accès à sa session, à ses données personnelles, à son compte, aux opérations qu'il pouvait effectuer sur le site, ...
Pour plus d'informations, n'hésitez pas à jeter un coup d'œil sur les nombreux documents qui existent sur le sujet ; par exemple : http://www.owasp.org/index.php/XSS (et, plus particulièrement, l'exemple "Cookie Grabber")
En espérant que cela vous apporte un début de réponse,
Bon courage !
Bonjour et merci de cette contribution
Je me demande s'il est possible d'envoyer des variables en POST plutôt que via une url en get comme avec un objet xhr ?
merci :)
Bonsoir,
L'URL indiquée en
srcde la balise<script>est chargée par le navigateur. Vu que, avec cette balise, il ne s'agit pas normalement pas d'envoyer des données au serveur, mais de charger un fichier, le navigateur envoi une requête GET.C'est l'usage que nous faisons ici de la balise
<script>qui est "anormal" ^^Donc, non, je ne pense pas qu'il soit possible d'envoyer une requête en POST de cette manière.
Salut.
Personellement j'utilise des iframes.
L'appelle à une balise script est elle asynchrone.
J'ai pas testé votre solution (encore)... Je vais le faire.
Merci a bientot.
Thanks a lot
Merci beaucoup
Gracias
well explanation
Bonjour,
Bravo pour l'article détaillé.
Je reprends la remarque de WebShaker. L'ajout de balise script rend le script synchrone. Dans Ajax, il y a Asynchrone. Je ne vois pas comment cette méthode rend asynchrone le script distant.
Pourrais-tu approfondir cette partie STP ?
Merci beaucoup.
fabscanta
Merci pour cet article, il m'a été très utile
(Désolé pour le doublon)
pour revenir sur les remarques de WebShaker et fabscanta, y a-t-il moyen d'interrompre l'exécution du script inséré ? (par exemple si la page appelée est indisponible)
j'ai lu que supprimer le Child inséré ne fonctionnait pas du moment que le script est chargé (ce qui est le cas ici) mais peut être y a-t-il un autre moyen..
Merci ! Bon article !
Pas mal du tout !
@WebShaker : les iFrame ne permettent pas le cross-domain, car elles deviennent inaccessibles depuis JavaScript lorsqu'elles font appel à un domaine différent de celui de la page parente...
Bonjour,
Cette stratégie de contournement est elle nécessaire lorsqu'on a accès au serveur cible? Accès en tant qu'administrateur.
Exellent, le seul inconvénient c'est qu'on doit contrôler le domaine distant et pouvoir mettre des fichier dessus...lol on peu pas faire une requêtes vers un site qui n'est pas le notre. quelle dommage!
Bonjour,
Merci Pascal pour ces informations. Je butte un peu sur ce probleme pour la page du moteur interne, qui pour le moment, est un iframe qui interroge le serveur distant des données, mais ce n'est pas la bonne méthode. J'essaye de rendre la V2 de mon annuaire conviviale, et je n'arrives pas a résoudre ce problème d'autorisation.
Je vais tester cette solution, car je n'utilise pas php et je n'arrives pas a donner les autorisations voulues a ma page distante pour afficher le contenu ajax.
Je met cette page en favori et donnerais le résultat
Marc L