Jouez en toute sécurité avec des iFrames en bac à sable

La création d'une expérience enrichie sur le Web d'aujourd'hui implique presque inévitablement d'intégrer des composants et des contenus sur lesquels vous n'avez aucun contrôle réel. Les widgets tiers peuvent stimuler l'engagement et jouer un rôle essentiel dans l'expérience utilisateur globale. Le contenu généré par l'utilisateur est parfois encore plus important que le contenu natif d'un site. S'abstenir de l'une ou l'autre n'est pas vraiment une option, mais les deux augmentent le risque que Quelque chose BadTM puisse se produire sur votre site. Chaque widget que vous intégrez (chaque annonce, chaque widget de réseau social) est un vecteur d'attaque potentiel pour les personnes à intention malveillante:

Content Security Policy (CSP) peut atténuer les risques associés à ces deux types de contenu en vous permettant d'ajouter à la liste blanche des sources de script et d'autres contenus spécifiquement approuvées. Il s'agit d'une étape majeure dans la bonne direction, mais il convient de noter que la protection offerte par la plupart des directives CSP est binaire: la ressource est autorisée ou non. Parfois, il peut être utile de dire : "Je ne suis pas sûr de faire confiance à cette source de contenu, mais c'est tellement joli ! Intégrez-le s'il vous plaît, navigateur, mais ne le laissez pas casser mon site. »

Premier privilège

Concrètement, nous recherchons un mécanisme nous permettant d'accorder un contenu dont nous n'intégrons que le niveau minimal de fonctionnalités nécessaires pour faire son travail. Si un widget n'a pas besoin d'ouvrir une nouvelle fenêtre, vous pouvez supprimer l'accès à window.open. S'il ne nécessite pas Flash, la désactivation de la prise en charge du plug-in ne devrait pas poser de problème. Nous protégeons autant que possible si nous appliquons le principe du moindre privilège et bloquons toutes les fonctionnalités qui ne sont pas directement liées à celles que nous souhaitons utiliser. Résultat : nous n'avons plus à croire aveuglément qu'un contenu intégré ne bénéficiera pas de droits qu'il ne devrait pas utiliser. Il n'aura simplement pas accès à la fonctionnalité en premier lieu.

Les éléments iframe constituent la première étape vers un framework adapté à ce type de solution. Le chargement d'un composant non approuvé dans un iframe offre une mesure de séparation entre votre application et le contenu que vous souhaitez charger. Le contenu encadré n'aura pas accès au DOM de votre page ni aux données stockées localement. Il ne pourra pas non plus dessiner à des positions arbitraires sur la page. Son champ d'application est limité au contour du cadre. Cependant, cette séparation n'est pas vraiment fiable. La page contenue comporte toujours un certain nombre d'options en cas de comportement agaçant ou malveillant: la lecture automatique de vidéos, de plug-ins et de pop-ups sont la partie visible de l'iceberg.

L'attribut sandbox de l'élément iframe nous fournit exactement ce dont nous avons besoin pour renforcer les restrictions sur le contenu encadré. Nous pouvons demander au navigateur de charger le contenu d'un frame spécifique dans un environnement à faibles privilèges, afin de n'autoriser que le sous-ensemble des fonctionnalités nécessaires pour effectuer le travail nécessaire.

Inverser, mais valider

Le bouton "Tweet" de Twitter est un excellent exemple de fonctionnalité qui peut être intégrée à votre site de manière plus sécurisée via un bac à sable. Twitter vous permet d'intégrer le bouton via un iFrame avec le code suivant:

<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
        style="border: 0; width:130px; height:20px;"></iframe>

Pour déterminer ce que nous pouvons verrouiller, examinons attentivement les fonctionnalités requises par le bouton. Le code HTML chargé dans le cadre exécute un bit de code JavaScript à partir des serveurs de Twitter et génère une fenêtre pop-up contenant une interface de tweet lorsque l'utilisateur clique dessus. Cette interface doit pouvoir accéder aux cookies de Twitter afin d'associer le tweet au bon compte et doit pouvoir envoyer le formulaire de tweet. C'est à peu près tout : le frame n'a pas besoin de charger de plug-ins ni de parcourir la fenêtre de premier niveau ni d'autres fonctionnalités. Comme il n'a pas besoin de ces droits, supprimons-les en créant un bac à sable pour le contenu du frame.

Le bac à sable fonctionne sur la base d'une liste blanche. Nous commençons par supprimer toutes les autorisations possibles, puis nous réactivons les fonctionnalités individuelles en ajoutant des indicateurs spécifiques à la configuration du bac à sable. Pour le widget Twitter, nous avons décidé d'activer JavaScript, les fenêtres pop-up, l'envoi de formulaires et les cookies de twitter.com. Pour ce faire, ajoutez un attribut sandbox à iframe avec la valeur suivante:

<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
    src="https://platform.twitter.com/widgets/tweet_button.html"
    style="border: 0; width:130px; height:20px;"></iframe>

C'est tout. Nous avons donné au frame toutes les fonctionnalités requises, et le navigateur lui refusera l'accès à tous les droits que nous ne lui avons pas accordés explicitement via la valeur de l'attribut sandbox.

Contrôle précis des fonctionnalités

Dans l'exemple ci-dessus, nous avons vu quelques-uns des indicateurs de bac à sable possibles. Explorons maintenant le fonctionnement interne de l'attribut plus en détail.

Dans le cas d'un iFrame avec un attribut "sandbox" vide, le document encadré est entièrement stocké dans un bac à sable, sous réserve des restrictions suivantes:

  • JavaScript ne s'exécutera pas dans le document encadré. Cela inclut non seulement le JavaScript chargé explicitement via des tags de script, mais aussi les gestionnaires d'événements intégrés et les URL javascript:. Cela signifie également que le contenu contenu dans les balises noscript s'affiche, exactement comme si l'utilisateur avait désactivé le script lui-même.
  • Le document encadré est chargé dans une origine unique, ce qui signifie que toutes les vérifications de même origine échoueront. Les origines uniques ne correspondent jamais à d'autres origines, pas même à elles-mêmes. Entre autres conséquences, cela signifie que le document n'a pas accès aux données stockées dans les cookies d'une origine ou à tout autre mécanisme de stockage (stockage DOM, base de données indexée, etc.).
  • Le document encadré ne peut pas créer de fenêtres ni de boîtes de dialogue (via window.open ou target="_blank", par exemple).
  • Impossible d'envoyer les formulaires.
  • Les plug-ins ne se chargent pas.
  • Le document encadré peut uniquement naviguer lui-même, pas son parent de niveau supérieur. La définition de window.top.location génère une exception, et le fait de cliquer sur le lien avec target="_top" n'a aucun effet.
  • Les fonctionnalités qui se déclenchent automatiquement (éléments de formulaire sélectionnés automatiquement, lecture automatique des vidéos, etc.) sont bloquées.
  • Impossible d'obtenir le verrouillage du pointeur.
  • L'attribut seamless est ignoré au niveau des iframes que contient le document encadré.

Cette approche est plutôt draconienne. Un document chargé dans un iframe entièrement en bac à sable ne présente en effet que très peu de risques. Bien sûr, cette solution ne présente pas non plus d'intérêt: vous pourrez peut-être vous en passer avec un bac à sable complet pour du contenu statique, mais la plupart du temps, vous aurez besoin d'assouplir les choses.

À l'exception des plug-ins, chacune de ces restrictions peut être levée en ajoutant un indicateur à la valeur de l'attribut sandbox. Les documents en bac à sable ne peuvent jamais exécuter de plug-ins, car ceux-ci sont du code natif hors bac à sable, mais tout le reste est juste:

  • allow-forms autorise l'envoi de formulaires.
  • allow-popups autorise les pop-ups (chocs).
  • allow-pointer-lock permet (surprise !) le verrouillage du pointeur.
  • allow-same-origin permet au document de conserver son origine. Les pages chargées à partir de https://example.com/ conservent l'accès aux données de cette origine.
  • allow-scripts permet l'exécution JavaScript et permet également de déclencher automatiquement des fonctionnalités (car elles sont simples à implémenter via JavaScript).
  • allow-top-navigation permet au document de sortir du cadre en naviguant dans la fenêtre de premier niveau.

En gardant cela à l'esprit, nous pouvons évaluer exactement pourquoi nous avons fini avec l'ensemble spécifique d'indicateurs de bac à sable dans l'exemple Twitter ci-dessus:

  • allow-scripts est requis, car la page chargée dans le frame exécute du code JavaScript pour gérer les interactions de l'utilisateur.
  • allow-popups est obligatoire, car le bouton fait apparaître un formulaire de tweet dans une nouvelle fenêtre.
  • allow-forms est requis, car il est possible d'envoyer le formulaire pour tweeter.
  • allow-same-origin est nécessaire, car les cookies de twitter.com seraient autrement inaccessibles, et l'utilisateur ne pourrait pas se connecter pour publier le formulaire.

Il est important de noter que les indicateurs de bac à sable appliqués à un frame s'appliquent également à toutes les fenêtres ou à tous les cadres créés dans le bac à sable. Cela signifie que nous devons ajouter allow-forms au bac à sable du frame, même si le formulaire n'existe que dans la fenêtre où le frame apparaît.

Une fois l'attribut sandbox en place, le widget n'obtient que les autorisations requises. Par ailleurs, des fonctionnalités telles que les plug-ins, la navigation supérieure et le verrouillage du pointeur restent bloquées. Nous avons réduit le risque d'intégration du widget, sans conséquence. Tout le monde y gagne.

Séparation des droits

La mise en bac à sable de contenu tiers afin d'exécuter son code non approuvé dans un environnement à faible privilège est manifestement bénéfique. Mais qu'en est-il de votre propre code ? Vous avez confiance en vous, n'est-ce pas ? Alors pourquoi s'inquiéter de l'environnement de bac à sable ?

Je pourrais répondre à la question suivante: si votre code n'a pas besoin de plug-ins, pourquoi lui donner accès à ces plug-ins ? Au mieux, c'est un privilège que vous n'utilisez jamais, au pire, c'est un vecteur potentiel pour les pirates informatiques d'entrer dans la porte. Le code de chacun comporte des bugs et pratiquement toutes les applications sont vulnérables d'une manière ou d'une autre. La mise en bac à sable de votre propre code signifie que même si un pirate informatique parvient à permuter votre application, il ne dispose pas d'un accès complet à l'origine de l'application. Il ne pourra effectuer que des opérations que l'application est susceptible de réaliser. Cette opération n'est pas aussi mauvaise, mais pas aussi grave qu'elle pourrait l'être.

Vous pouvez réduire encore davantage le risque en décomposant votre application en éléments logiques et en mettant chaque élément en bac à sable avec le minimum de privilèges possible. Cette technique est très courante dans le code natif. Chrome, par exemple, se rompt en un processus de navigateur à privilèges élevés qui a accès au disque dur local et peut établir des connexions réseau, ainsi que de nombreux processus de moteur de rendu à faible privilège qui effectuent la charge de travail de l'analyse du contenu non approuvé. Les moteurs de rendu n'ont pas besoin de toucher le disque : le navigateur se charge de leur fournir toutes les informations nécessaires pour afficher une page. Même si une hacker intelligente trouve un moyen de corrompre un moteur de rendu, elle n'est pas allée très loin, car le moteur de rendu ne peut pas faire grand-chose par lui-même : tous les accès à privilèges élevés doivent être acheminés via le processus du navigateur. Les pirates informatiques doivent trouver plusieurs trous dans différentes parties du système pour causer des dégâts, ce qui réduit considérablement le risque de réussite du pwn.

Bac à sable sécurisé pour eval()

Avec le bac à sable et l'API postMessage, la réussite de ce modèle est assez simple à appliquer au Web. Des parties de votre application peuvent se trouver dans des iframe de bac à sable, et le document parent peut brouiller la communication entre elles en publiant des messages et en écoutant les réponses. Avec ce type de structure, les exploits dans une partie de l'application réduisent au minimum les dégâts possibles. Elle présente également l'avantage de vous obliger à créer des points d'intégration clairs, de sorte que vous sachiez exactement où vous devez faire attention à la validation des entrées et des sorties. Prenons l'exemple d'un jouet, pour voir comment cela pourrait fonctionner.

Evalbox est une application intéressante qui prend une chaîne et l'évalue en tant que JavaScript. Waouh, non ? C’est exactement ce que vous attendiez depuis toutes ces longues années. Il s'agit d'une application assez dangereuse, car autoriser l'exécution de JavaScript arbitraire implique que toutes les données d'une origine peuvent être récupérées. Nous allons réduire le risque de mauvaises activitésTM en veillant à ce que le code soit exécuté dans un bac à sable, ce qui le rend beaucoup plus sûr. Nous allons parcourir le code de l'intérieur vers l'extérieur, en commençant par le contenu du frame:

<!-- frame.html -->
<!DOCTYPE html>
<html>
    <head>
    <title>Evalbox's Frame</title>
    <script>
        window.addEventListener('message', function (e) {
        var mainWindow = e.source;
        var result = '';
        try {
            result = eval(e.data);
        } catch (e) {
            result = 'eval() threw an exception.';
        }
        mainWindow.postMessage(result, event.origin);
        });
    </script>
    </head>
</html>

Dans le cadre, nous avons un document minimal qui écoute simplement les messages de son parent en établissant un hooks à l'événement message de l'objet window. Chaque fois que le parent exécute postMessage sur le contenu de l'iFrame, cet événement se déclenche et nous donne accès à la chaîne que notre parent souhaite que nous exécutions.

Dans le gestionnaire, nous récupérons l'attribut source de l'événement, qui est la fenêtre parente. Nous l'utiliserons pour renvoyer le résultat de notre travail acharné une fois que nous aurons terminé. Ensuite, nous ferons le plus gros du travail en transmettant les données qui nous ont été fournies dans eval(). Cet appel a été encapsulé dans un bloc try, car les opérations interdites dans un iframe en bac à sable génèrent fréquemment des exceptions DOM. Nous les interceptons et affichons un message d'erreur convivial à la place. Enfin, nous publions le résultat dans la fenêtre parent. C'est assez simple.

Le parent est tout aussi simple. Nous allons créer une toute petite interface utilisateur avec un textarea pour le code et un button pour l'exécution, et nous allons récupérer frame.html via un iframe en bac à sable, autorisant uniquement l'exécution du script:

<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
        id='sandboxed'
        src='frame.html'></iframe>

Nous allons maintenant brancher les éléments pour exécution. Tout d'abord, nous allons écouter les réponses de iframe et de alert() à nos utilisateurs. Une application réelle effectuerait quelque chose de moins ennuyeux:

window.addEventListener('message',
    function (e) {
        // Sandboxed iframes which lack the 'allow-same-origin'
        // header have "null" rather than a valid origin. This means you still
        // have to be careful about accepting data via the messaging API you
        // create. Check that source, and validate those inputs!
        var frame = document.getElementById('sandboxed');
        if (e.origin === "null" &amp;&amp; e.source === frame.contentWindow)
        alert('Result: ' + e.data);
    });

Nous allons ensuite connecter un gestionnaire d'événements aux clics sur button. Lorsque l'utilisateur clique, nous récupérons le contenu actuel de textarea et le transmettons dans le frame pour exécution:

function evaluate() {
    var frame = document.getElementById('sandboxed');
    var code = document.getElementById('code').value;
    // Note that we're sending the message to "*", rather than some specific
    // origin. Sandboxed iframes which lack the 'allow-same-origin' header
    // don't have an origin which you can target: you'll have to send to any
    // origin, which might alow some esoteric attacks. Validate your output!
    frame.contentWindow.postMessage(code, '*');
}

document.getElementById('safe').addEventListener('click', evaluate);

Simple, n'est-ce pas ? Nous avons créé une API d'évaluation très simple. Nous pouvons être sûrs que le code évalué n'a pas accès à des informations sensibles telles que les cookies ou le stockage DOM. De même, le code évalué ne peut pas charger de plug-ins, ouvrir de nouvelles fenêtres pop-up ni aucune autre activité agaçante ou malveillante.

Vous pouvez faire de même pour votre propre code en décomposant des applications monolithiques en composants à application unique. Chacun d'entre eux peut être encapsulé dans une API de messagerie simple, comme nous l'avons écrit ci-dessus. La fenêtre parent à privilèges élevés peut servir de contrôleur et de coordinateur, en envoyant des messages à des modules spécifiques disposant chacun des moins de privilèges possible pour effectuer leurs tâches, en écoutant les résultats et en s'assurant que chaque module ne reçoit que les informations dont il a besoin.

Notez toutefois que vous devez faire très attention lorsque vous traitez du contenu encadré qui provient de la même origine que le parent. Si une page sur https://example.com/ encadre une autre page de la même origine avec un bac à sable qui inclut les indicateurs allow-same-origin et allow-scripts, la page encadrée peut atteindre le parent et supprimer entièrement l'attribut sandbox.

Jouez dans votre bac à sable

Le bac à sable est actuellement disponible dans de nombreux navigateurs: Firefox 17 et versions ultérieures, Internet Explorer 10 et versions ultérieures et Chrome au moment de la rédaction de ce document (la table d'assistance est bien sûr à jour). L'application de l'attribut sandbox à iframes que vous incluez vous permet d'accorder certains droits sur le contenu qu'il affiche, uniquement les droits nécessaires au bon fonctionnement du contenu. Cela vous permet de réduire les risques associés à l'inclusion de contenu tiers, au-delà de ce qui est déjà possible avec Content Security Policy.

De plus, le bac à sable est une technique efficace pour réduire le risque qu'un pirate informatique malintentionné puisse exploiter des failles dans votre propre code. En séparant une application monolithique en un ensemble de services en bac à sable, chacun responsable d'un petit fragment de fonctionnalités autonomes, les pirates informatiques seront contraints non seulement de compromettre le contenu de frames spécifiques, mais aussi leur contrôleur. Cette tâche est beaucoup plus difficile, d'autant plus que le champ d'application du contrôleur peut être considérablement réduit. Vous pouvez consacrer vos efforts de sécurité à auditer ce code si vous demandez de l'aide au navigateur pour le reste.

Cela ne veut pas dire que le bac à sable est une solution complète au problème de la sécurité sur Internet. Il offre une défense en profondeur. À moins que vous ne contrôliez les clients de vos utilisateurs, vous ne pouvez pas encore compter sur la compatibilité du navigateur pour tous vos utilisateurs (si vous contrôlez les clients de vos utilisateurs, par exemple dans un environnement d'entreprise, Hourra !). Un jour... mais pour l'instant, le bac à sable est une couche de protection supplémentaire qui permet de renforcer vos défenses. Il ne s'agit pas d'une défense complète sur laquelle vous pouvez uniquement compter. Pourtant, les calques sont excellents. Je vous suggère d'utiliser celle-ci.

Documentation complémentaire

  • Séparation des droits dans les applications HTML5 est un article intéressant qui présente la conception d'un petit framework et son application à trois applications HTML5 existantes.

  • Le bac à sable peut être encore plus flexible lorsqu'il est associé à deux autres nouveaux attributs iFrame: srcdoc et seamless. Le premier vous permet de remplir un frame avec du contenu sans les frais généraux d'une requête HTTP, et le second permet au style de s'insérer dans le contenu frame. La prise en charge des deux navigateurs est actuellement plutôt médiocre (Chrome et WebKit...), mais constituera une combinaison intéressante à l'avenir. Vous pouvez, par exemple, placer dans le bac à sable les commentaires d'un article à l'aide du code suivant:

        <iframe sandbox seamless
                srcdoc="<p>This is a user's comment!
                           It can't execute script!
                           Hooray for safety!</p>"></iframe>