La création d'une expérience riche sur le Web d'aujourd'hui implique presque inévitablement d'intégrer des composants et du contenu sur lesquels vous n'avez aucun contrôle réel. Les widgets tiers peuvent générer de l'engagement et jouer un rôle essentiel dans l'expérience utilisateur globale. Les contenus générés par les utilisateurs sont parfois encore plus importants que les contenus natifs d'un site. S'abstenir de l'un ou de l'autre n'est pas vraiment une option, mais les deux augmentent le risque que quelque chose de mauvais™ se produise 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 malveillantes:
La Content Security Policy (CSP) peut atténuer les risques associés à ces deux types de contenus en vous permettant d'ajouter à la liste blanche des sources de script et d'autres contenus particulièrement fiables. Il s'agit d'une avancée 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. Il peut être utile de dire parfois : "Je ne suis pas sûr de faire confiance à cette source de contenu, mais elle est tellement jolie ! Intégrez-le, s'il vous plaît, navigateur, mais ne laissez pas cela endommager mon site."
Principe du moindre privilège
En substance, nous recherchons un mécanisme qui nous permettra d'accorder au contenu que nous intégrons uniquement le niveau minimal de fonctionnalités nécessaires à son fonctionnement. Si un widget n'a pas besoin d'afficher une nouvelle fenêtre, il est préférable de supprimer l'accès à window.open. Si elle ne nécessite pas Flash, la désactivation de la prise en charge des plug-ins ne devrait pas poser de problème. Nous garantissons une sécurité maximale en suivant le principe du moindre privilège et en bloquant toutes les fonctionnalités qui ne sont pas directement pertinentes pour la fonctionnalité que nous souhaitons utiliser. Par conséquent, nous n'avons plus à faire confiance aveuglément à un élément de contenu intégré pour qu'il n'exploite pas des privilèges qu'il ne devrait pas utiliser. Il n'aura tout simplement pas accès à la fonctionnalité.
Les éléments iframe
constituent la première étape vers un bon framework pour une telle solution.
Le chargement d'un composant non approuvé dans une iframe
permet de séparer votre application du contenu que vous souhaitez charger. Le contenu encadré n'a pas accès au DOM de votre page ni aux données que vous avez stockées localement. Il ne peut pas non plus dessiner à des positions arbitraires sur la page. Son champ d'application est limité au contour du frame. Toutefois, cette séparation n'est pas vraiment robuste. La page contenue comporte toujours un certain nombre d'options pour un comportement gênant ou malveillant: les vidéos en lecture automatique, les plug-ins et les pop-ups ne sont que 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 à faible privilège, en n'autorisant que le sous-ensemble de fonctionnalités nécessaires pour effectuer le travail requis.
Faire confiance, mais vérifier
Le bouton "Tweet" de Twitter est un excellent exemple de fonctionnalité pouvant être intégrée plus en toute sécurité à votre site 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 frame exécute un peu de code JavaScript à partir des serveurs de Twitter et génère une fenêtre pop-up remplie d'une interface de tweet lorsque l'utilisateur clique dessus. Cette interface doit avoir accès aux cookies de Twitter pour 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 naviguer dans la fenêtre de niveau supérieur, ni de nombreux autres éléments de fonctionnalité. Comme il n'a pas besoin de ces droits, supprimons-les en mettant le contenu du frame en bac à sable.
Le bac à sable fonctionne sur la base d'une liste blanche. Nous commençons par supprimer toutes les autorisations possibles, puis 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 pop-ups, 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>
Et voilà ! Nous avons attribué au frame toutes les fonctionnalités dont il a besoin, et le navigateur lui refusera l'accès à tous les privilèges que nous ne lui avons pas explicitement accordés via la valeur de l'attribut sandbox
.
Contrôle précis des fonctionnalités
Nous avons vu quelques-uns des indicateurs de bac à sable possibles dans l'exemple ci-dessus. Examinons maintenant le fonctionnement interne de l'attribut plus en détail.
Si vous utilisez un iFrame avec un attribut de bac à sable vide, le document encadré sera entièrement placé dans un bac à sable, ce qui le soumet aux restrictions suivantes:
- JavaScript ne s'exécute pas dans le document encadré. Cela inclut non seulement le code JavaScript chargé explicitement via des balises de script, mais également 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, même à elles-mêmes. Cela signifie, entre autres, que le document n'a pas accès aux données stockées dans les cookies d'une origine ni à 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
outarget="_blank"
, par exemple). - Les formulaires ne peuvent pas être envoyés.
- Les plug-ins ne se chargent pas.
- Le document encadré ne peut naviguer que vers lui-même, et non vers son parent de niveau supérieur.
Définir
window.top.location
génère une exception, et cliquer sur le lien avectarget="_top"
n'a aucun effet. - Les fonctionnalités qui se déclenchent automatiquement (éléments de formulaire avec mise au point automatique, vidéos en lecture automatique, etc.) sont bloquées.
- Impossible d'obtenir le verrouillage du pointeur.
- L'attribut
seamless
est ignoré suriframes
que le document encadré contient.
C'est très draconien, et un document chargé dans un iframe
entièrement en bac à sable ne présente que très peu de risques. Bien entendu, il ne peut pas non plus être très utile: vous pouvez vous contenter d'un bac à sable complet pour certains contenus statiques, mais la plupart du temps, vous devrez assouplir les choses un peu.
À l'exception des plug-ins, chacune de ces restrictions peut être levée en ajoutant un indicateur à la valeur de l'attribut bac à sable. Les documents en bac à sable ne peuvent jamais exécuter de plug-ins, car les plug-ins sont du code natif non en bac à sable, mais tout le reste est permis:
allow-forms
permet d'envoyer le formulaire.allow-popups
autorise (surprise !) les pop-up.allow-pointer-lock
permet (surprise !) de verrouiller le pointeur.allow-same-origin
permet au document de conserver son origine. Les pages chargées à partir dehttps://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 seraient faciles à implémenter via JavaScript).allow-top-navigation
permet au document de sortir du frame en parcourant la fenêtre de niveau supérieur.
Compte tenu de ces éléments, nous pouvons évaluer exactement pourquoi nous avons obtenu l'ensemble spécifique d'indicateurs de bac à sable dans l'exemple Twitter ci-dessus:
allow-scripts
est obligatoire, car la page chargée dans le frame exécute du code JavaScript pour gérer l'interaction utilisateur.allow-popups
est obligatoire, car le bouton affiche un formulaire de tweet dans une nouvelle fenêtre.allow-forms
est obligatoire, car le formulaire de tweet doit pouvoir être envoyé.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.
Notez que les indicateurs de bac à sable appliqués à un frame s'appliquent également à toutes les fenêtres ou frames créées 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 dans laquelle le frame s'affiche.
Avec l'attribut sandbox
en place, le widget n'obtient que les autorisations dont il a besoin, et les fonctionnalités telles que les plug-ins, la navigation en haut de l'écran et le verrouillage du pointeur restent bloquées. Nous avons réduit le risque d'intégration du widget, sans effet négatif.
Tout le monde y gagne.
Séparation des droits
Il est assez évident que le bac à sable de contenu tiers pour exécuter leur code non approuvé dans un environnement à privilèges limités est bénéfique. Mais qu'en est-il de votre propre code ? Vous avez confiance en vous, n'est-ce pas ? Pourquoi s'inquiéter du bac à sable ?
Je voudrais inverser la question: si votre code n'a pas besoin de plug-ins, pourquoi lui donner accès à des plug-ins ? Au mieux, il s'agit d'un privilège que vous n'utilisez jamais. Au pire, il s'agit d'un vecteur potentiel pour les pirates informatiques pour pénétrer dans votre système. Le code de tout le monde contient des bugs, et pratiquement toutes les applications sont vulnérables à l'exploitation d'une manière ou d'une autre. Si vous mettez votre propre code en bac à sable, même si un pirate informatique parvient à subvertir votre application, il ne disposera pas d'un accès complet à l'origine de l'application. Il ne pourra effectuer que les actions que l'application pourrait effectuer. Cela reste mauvais, mais pas aussi grave que cela pourrait l'être.
Vous pouvez réduire encore plus le risque en divisant votre application en éléments logiques et en mettant chaque élément en bac à sable avec les droits d'accès les plus limités possible. Cette technique est très courante dans le code natif: Chrome, par exemple, se scinde en un processus de navigateur à privilèges élevés qui a accès au disque dur local et peut établir des connexions réseau, et de nombreux processus de rendu à privilèges faibles qui effectuent la tâche ardue d'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 dont ils ont besoin pour afficher une page. Même si un pirate informatique astucieux trouve le moyen de corrompre un moteur de rendu, il n'ira pas très loin, car le moteur de rendu ne peut pas faire grand-chose par lui-même : tous les accès à privilège élevé doivent être acheminés via le processus du navigateur. Les pirates informatiques doivent trouver plusieurs failles dans différentes parties du système pour causer des dommages, ce qui réduit considérablement le risque de piratage.
Mettre eval()
en bac à sable de manière sécurisée
Avec le bac à sable et l'API postMessage
, le succès de ce modèle est assez simple à appliquer au Web. Des éléments de votre application peuvent résider dans des iframe
s en bac à sable, et le document parent peut assurer la médiation de la communication entre eux en publiant des messages et en écoutant les réponses. Ce type de structure garantit que les exploits dans une partie de l'application causent le moins de dégâts possible. Il présente également l'avantage de vous obliger à créer des points d'intégration clairs, ce qui vous permet de savoir exactement où vous devez faire attention lors de la validation des entrées et des sorties. Prenons un exemple fictif pour voir comment cela fonctionne.
Evalbox est une application intéressante qui prend une chaîne et l'évalue en tant que code JavaScript. C'est incroyable, non ? C'est exactement ce que vous attendiez depuis toutes ces longues années. Il s'agit bien sûr d'une application assez dangereuse, car autoriser l'exécution de code JavaScript arbitraire signifie que toutes les données qu'une origine peut offrir sont à prendre. Nous allons atténuer le risque de "mauvaises choses" en nous assurant que le code est exécuté dans un bac à sable, ce qui le rend beaucoup plus sûr. Nous allons examiner 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 frame, nous avons un document minimal qui écoute simplement les messages de son parent en s'accrochant à 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, ce qui nous donne accès à la chaîne que le parent souhaite que nous exécutions.
Dans le gestionnaire, nous récupérons l'attribut source
de l'événement, qui correspond à la fenêtre parente. Nous l'utiliserons pour renvoyer le résultat de notre travail en amont une fois que nous aurons terminé. Nous allons ensuite effectuer la majeure partie du travail en transmettant les données qui nous ont été fournies à 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 intercepterons et signalerons plutôt un message d'erreur convivial. Enfin, nous publions le résultat dans la fenêtre parente. Il s'agit d'une procédure assez simple.
Le parent est tout aussi simple. Nous allons créer une petite UI avec un textarea
pour le code et un button
pour l'exécution, et nous allons extraire frame.html
via un iframe
en bac à sable, n'autorisant que l'exécution de 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 connecter les éléments pour l'exécution. Tout d'abord, nous écoutons les réponses de l'iframe
et les alert()
à nos utilisateurs. Une application réelle ferait probablement 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" && e.source === frame.contentWindow)
alert('Result: ' + e.data);
});
Ensuite, nous allons 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 au frame pour l'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);
Facile, n'est-ce pas ? Nous avons créé une API d'évaluation très simple, et 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, afficher de nouvelles fenêtres ni effectuer un certain nombre d'autres activités gênantes ou malveillantes.
Vous pouvez faire de même pour votre propre code en divisant les applications monolithiques en composants à usage unique. Chacune peut être encapsulée dans une API de messagerie simple, comme nous l'avons indiqué ci-dessus. La fenêtre parente à privilèges élevés peut agir en tant que contrôleur et répartiteur, en envoyant des messages dans des modules spécifiques qui ont chacun le moins de privilèges possible pour effectuer leur travail, en écoutant les résultats et en veillant à ce que chaque module ne reçoive que les informations dont il a besoin.
Notez toutefois que vous devez faire très attention lorsque vous traitez du contenu encadré provenant 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 à la fois les indicateurs allow-same-origin et allow-scripts, la page encadrée peut atteindre le parent et supprimer entièrement l'attribut bac à sable.
Jouer dans votre bac à sable
Le bac à sable est disponible dans de nombreux navigateurs: Firefox 17 et versions ultérieures, IE 10 et versions ultérieures, et Chrome au moment de la rédaction de cet article (caniuse propose bien sûr un tableau de compatibilité à jour). Appliquer l'attribut sandbox
aux iframes
que vous incluez vous permet d'accorder certains droits au contenu qu'ils affichent, uniquement ceux qui sont nécessaires au bon fonctionnement du contenu. Vous pouvez ainsi réduire le risque associé à l'inclusion de contenu tiers, au-delà de ce qui est déjà possible avec le Règlement sur la sécurité du contenu.
De plus, le bac à sable est une technique efficace pour réduire le risque qu'un pirate informatique astucieux puisse exploiter des failles dans votre propre code. En séparant une application monolithique en un ensemble de services en bac à sable, chacun étant responsable d'une petite partie de la fonctionnalité autonome, les pirates informatiques seront contraints de compromettre non seulement le contenu de cadres spécifiques, mais aussi leur contrôleur. C'est une tâche beaucoup plus difficile, en particulier parce que le contrôleur peut avoir une portée considérablement réduite. Vous pouvez consacrer vos efforts liés à la sécurité à l'audit de ce code si vous demandez au navigateur de vous aider pour le reste.
Cela ne veut pas dire que le bac à sable est une solution complète au problème de sécurité sur Internet. Il offre une défense en profondeur, et à moins que vous ne contrôliez les clients de vos utilisateurs, vous ne pouvez pas encore compter sur la prise en charge du navigateur pour tous vos utilisateurs (si vous contrôlez les clients de vos utilisateurs, un environnement d'entreprise, par exemple, bravo !). Un jour peut-être… mais pour l'instant, le bac à sable est une autre couche de protection pour renforcer vos défenses. Il ne s'agit pas d'une défense complète sur laquelle vous pouvez vous appuyer uniquement. Les calques restent excellents. Je vous suggère d'utiliser celle-ci.
Documentation complémentaire
Privilege Separation in HTML5 Applications est un article intéressant qui explique la conception d'un petit framework et son application à trois applications HTML5 existantes.
Le bac à sable peut être encore plus flexible lorsqu'il est combiné à deux autres nouveaux attributs d'iframe:
srcdoc
etseamless
. 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'appliquer au contenu encadré. Pour le moment, la compatibilité avec les navigateurs est assez médiocre (versions nocturnes de Chrome et WebKit), mais ce sera une combinaison intéressante à l'avenir. Vous pouvez, par exemple, mettre en bac à sable les commentaires sur 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>