Éviter le détournement de clic par iFrame de votre site

J'en vois déjà venir d'assez loin : « les iFrames c'est old school ». Ça me rappel l'époque ou les Frames « c'était old school ». Pour les gars du fond, une iFrame permet d'insérer dans la page courante d'un site le contenu complet d'une autre page. Et si vous ne vous y intéressez plus car vous n'en voyez pas l'intérêt, sachez que d'autres peuvent le voir.

Effectivement, vous n'êtes pas à l'abri de retrouver une page de votre site dans l'iFrame d'un autre site. À partir de là, pas mal de scénarios sont envisageables ; du moins dérangeant comme la solicitiation de votre serveur à chaque fois que la page du site embarquant votre page est réclamée aux plus génants comme le détournement de clic (clickjacking).

Contenu d'un site distant dans une iFrame

Pour illustrer le cas de figure par défault (on ne demande rien de précis au serveur contre les iFrames), nous allons utiliser le simple code suivant qui va embarquer une page de Wikipedia :

HTML

<iframe src="https://fr.wikipedia.org/wiki/Wikip%C3%A9dia:Accueil_principal" width="100%" height="200px" scrolling="no"></iframe>

Résultat

Cela signifie qu'à chaque fois que vous lirez cet article, des requêtes seront faites aux serveurs hébergeant Wikipedia pour l'afficher dans cette page. Pas très génant (sauf si le site en question n'a pas une bande passante illimité par mois).

Petit exemple de détournement de clic

Avec le changement de code suivant, nous allons nous amuser à modifier la page. Nous allons voir facilement en haut à gauche le texte « WikiMOI HA HA ! » et cliquer dessus reviendra sur cet article. Mais on pourrait tout aussi bien vous faire croire que vous allez faire un don à Wikipedia...

Pour voir cela, il faudra au préalable cliquer sur « Tricher ! ».

HTML

<button>Tricher !</button>
<div>WikiMOI<br>HA HA !</div>
<iframe src="https://fr.wikipedia.org/wiki/Wikip%C3%A9dia:Accueil_principal" width="100%" height="200px" scrolling="no"></iframe>

CSS

div {
    display: none;
}
.fake {
    width: 100%;
    height: 100%;
    overflow: hidden;
    position: fixed;
    top: 0;
    left: 0;
}
.fake-div {
    display: block;
    position: fixed;
    width: 160px;
    top: 119px;
    left: 2px;
    z-index: 1001;
    background-color: #f6f6f6;
    text-align: center;
    font-weight: bold;
    color: #f00;
    font-family: arial;
    font-size: 20px;
    cursor: pointer;
}

JS

// Mettre en place la triche.
document.getElementsByTagName("button")[0].addEventListener("click", function () {
    document.getElementsByTagName("iframe")[0].classList.add("fake");
    document.getElementsByTagName("div")[0].classList.add("fake-div");
});
// Détournement de clique !!! (Ici on revient en arrière)
document.getElementsByTagName("div")[0].addEventListener("click", function () {
    document.getElementsByTagName("iframe")[0].classList.remove("fake");
    document.getElementsByTagName("div")[0].classList.remove("fake-div");
});

Résultat

Cliquez sur le bouton « Tricher ! »

WikiMOI
HA HA !

On constate assez facilement que permettre l'affichage de votre site par d'autre site peut poser problème si vous n'avez pas confiance en ces sites.

X-Frame-Options : afficher votre site dans une iFrame

Pour décider vous même quels sont les sites qui vont pouvoir ou non afficher une de vos pages dans une iFrame, vous aller pouvoir utiliser l'en-tête HTTP X-Frame-Options (RFC 7034).

X-Frame-Options est une en-tête HTTP, ce qui signifie qu'il se trouvera dans la réponse HTTP du serveur quand celui-ci répondra à la requête d'un navigateur pour afficher une page.

Donc, dans le cas précédent, c'est au site Wikipedia d'ajouter cette en-tête sur ses pages pour que, si elles sont réclamées dans une iFrame, celles-ci ne puissent pas s'afficher.

Valeurs de X-Frame-Options

Les deux options de base sont SAMEORIGIN et DENY et sont supportées par :

  • Google Chrome 4+
  • Internet Explorer 8+
  • Safari 4+
  • Mozilla Firefox 3.6+

Il existe une troisième option ALLOW-FROM qui permet de choisir exactement qui peut ou non afficher le contenu. Voyons cela en détail.

SAMEORIGIN

L'en-tête suivante X-Frame-Options: SAMEORIGIN ajouter aux en-têtes HTTP d'une réponse de page va autoriser celle-ci à être affichée dans une iFrame uniquement si la page l'appelant se trouve sur le même nom de domaine. Dans notre cas précédent, si Wikipédia utilisait cette en-tête, seule une page de Wikipedia pourrait l'afficher dans une iFrame et aucun autre site.

DENY

L'en-tête suivante X-Frame-Options: DENY ajouter aux en-têtes HTTP d'une réponse de page est rédibitoire. Elle signifie que peut importe qui demande à afficher la page dans une iFrame, celle-ci ne s'affichera pas, même si elle appartient au même domaine.

ALLOW-FROM

L'en-tête suivante X-Frame-Options: ALLOW-FROM www.domain.com ajouter aux en-têtes HTTP d'une réponse de page va autoriser celle-ci à être affichée dans une iFrame du site www.domain.com. Cela est effectivement le cas pour mon blog.

Exemple sur ce Blog

Si vous vérifiez les en-têtes HTTP de la réponse de l'article « NodeAtlas, le Framework JavaScript MVC(2), SEO et W3C compliant », vous constaterez qu'il y a bien une en-tête HTTP X-Frame-Options: ALLOW-FROM https://www.lesieur.name/ ce qui permet à la page de présentation de NodeAtlas de mon portfolio de l'afficher dans une iFrame.

Les navigateurs refusent l'affichage

Comprenez bien que ce mécanisme fonctionne car les navigateurs ont pour ordre de ne pas afficher un contenu dans une iFrame s'ils ne trouvent pas l'en-tête HTTP X-Frame-Options autorisant l'affichage de se contenu (ou aucune en-tête X-Frame-Options). Cela est donc une sécurité pour le client final en ce qui concerne le détournement de clic mais pas pour la charge serveur puisque quoi qu'il arrive, il répondra au navigateur.

Différent comportement par navigateur

À titre d'exemple, Firefox n'affichera rien mais indiquera dans la console le soucis alors que Edge affichera un message comme celui-ci : « Nous ne pouvons pas afficher ce contenu dans un cadre ».

Content-Security-Policy et Webkit

Cependant ALLOW-FROM n'est pas reconnue par Webkit ce qui conduit à l'erreur suivante Invalid 'X-Frame-Options' header encountered when loading 'www.domain.com/example': 'ALLOW-FROM www.domain.com' is not a recognized directive. The header will be ignored. et donc autorise l'affichage de l'iFrame par tout le monde.

Il faut dans ce cas se tourner vers l'en-tête HTTP Content-Security-Policy: frame-ancestors www.domain.com pour qu'un mécanisme similaire fonctionne.

Exemple d'utilisation avec NodeAtlas

Vous pouvez bien entendu utiliser ses en-têtes avec Apache, nginx ou directement dans votre code PHP, Ruby, etc.

Nous allons voir ici comment cela se traduit en JavaScript côté serveur avec le module npm node-atlas de Node.js.

NodeAtlas est un Framework JavaScript MVC(2) côté serveur vous permettant de créer des sites évolutifs, SEO-compliant et W3C-compliant. À ce titre il peut également utiliser les mécanismes précédemment cités, voici comment :

Par page, avec le webconfig

webconfig.json par exemple

{
    "routes": {
        "/": {    
            "view": "index.htm",
            "headers": {
                "X-Frame-Origins": "ALLOW-FROM www.lesieur.name",
                "Content-Security-Policy": "frame-ancestors www.lesieur.name"
            }
        }
    }
}

Tout le site, avec le webconfig

{
    "headers": {
        "X-Frame-Origins": "ALLOW-FROM www.lesieur.name",
        "Content-Security-Policy": "frame-ancestors www.lesieur.name"
    }
    "routes": {
        "/": "index.htm"
    }
}

Par page, avec le contrôleur spécifique

controllers/index.js par exemple

exports.changeVariations = function (next, locals, request, response) {

    response.setHeader("X-Frame-Options", "ALLOW-FROM www.lesieur.name");
    response.setHeader("Content-Security-Policy", "frame-ancestors www.lesieur.name");

    next();
};

Tout le site, avec le contrôleur commun

controllers/common.js par exemple

exports.setConfigurations = function (next) {
    var NA = this;

    NA.express.use(function (request, response, next) {
        response.setHeader("X-Frame-Options", "ALLOW-FROM www.lesieur.name");
        response.setHeader("Content-Security-Policy", "frame-ancestors www.lesieur.name");
        next();
    });

    next();
};