Les iframes et notre but
TL;DR : Les iframes permettent d'intégrer une page web dans une autre page web. Et on va les utiliser pour créer un hub d'application. (doc : MDN)
L'élément HTML <iframe>
représente un contexte de navigation imbriqué qui permet en fait d'obtenir une page HTML intégrée dans la page courante. Dans un sens, c'est une manière de placer une autre page HTML dans une page HTML.
Chaque contexte de navigation possède son propre historique et son propre document actif. Le contexte de navigation qui contient le contenu intégré est appelé « contexte de navigation parent ». Le contexte de navigation le plus élevé correspond généralement à la fenêtre du navigateur.
Les navigateurs web permettent ainsi d'insérer des iframes dans des pages web pour afficher une autre page web, une vidéo, un document PDF, etc.
Dans notre cas, nous allons utiliser les iframes pour créer une page web qui regroupe plusieurs pages web, comme un hub de navigation. (il sera question de myisima, d'un Homer et un tas d'autres services, de façon la plus modulaire possible)
Sommaire
-
Les iframes et notre but
- Sommaire
- 1. Sécurité, points d'attentions et solutions
- 2. Communication entre iframes et parent
- Exemple via docker compose
1. Sécurité, points d'attentions et solutions
Les iframes, comme beaucoup d'autre éléments du web, peuvent être soumis à des attaques. Il est donc important de prendre en compte ces points d'attentions et de mettre en place des solutions pour les contrer. Même si tous les prochains points abordés ne sont pas forcément d'actualité dans notre cas, il est important de les connaître pour les appliquer si besoin. (puis un peu de culture , ça ne fait pas de mal)
1.1 CSRF (Clickjacking)
Problème : Un attaquant peut charger votre application dans une iframe cachée ou transparente sur son propre site. Ensuite, il peut tromper l'utilisateur en lui faisant cliquer sur des éléments de votre application sans qu'il s'en rende compte.
Exemple simple : Imaginez une page de paiement intégrée dans une iframe sur un site malveillant. L'utilisateur croit cliquer sur une offre promotionnelle mais, en réalité, il valide un paiement sur votre application.
frame-ancestors
Solution : CSP avec (Ancienne méthode via l'en-tête HTTP X-Frame-Options
, même si en vrai idc)
La directive frame-ancestors
de la Content-Security-Policy (CSP) spécifie les parents pouvant intégrer une page en utilisant une iframe.
Exemple :
Pour autoriser uniquement example.com
à intégrer votre iframe :
Content-Security-Policy: frame-ancestors 'self' https://example.com;
1.2 Cross-Site Scripting (XSS)
Problème : Les iframes peuvent afficher du contenu externe. Si ce contenu contient des scripts malveillants, ces scripts peuvent compromettre la sécurité de votre application.
Exemple simple : Un script injecté dans l'iframe peut voler des cookies, des tokens d'authentification, ou manipuler l'affichage pour tromper l'utilisateur.
sandbox
Solution 1 : Attribut L'attribut sandbox
applique des restrictions sur le contenu qui peut apparaître dans l'iframe.
Exemple :
<iframe src="https://example.com" sandbox></iframe>
Si cet attribut vaut la chaîne de caractères vide, toutes les restrictions sont appliquées, sinon, on peut utiliser une liste de mots-clés séparés par des espaces pour définir des restrictions précises.
Par défaut, sandbox
restreint :
- L'exécution de scripts.
- Les formulaires.
- Les accès aux cookies. (si j'ai bien compris, comme
allow-storage-access-by-user-activation
etallow-same-origin
ne sont pas spécifiés) - Les fenêtres contextuelles.
sandbox
Personnalisation des permissions avec Vous pouvez ajouter des valeurs pour accorder certaines permissions spécifiques :
-
allow-scripts
: Permet les scripts. -
allow-same-origin
: Permet l'accès aux cookies / stockage local. -
allow-forms
: Permet les soumissions de formulaires.
Ce qui donne :
<iframe src="https://example.com" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
Content-Security-Policy (CSP)
Solution 2 :Ajoutez une CSP pour limiter les scripts, styles, images, etc., qui peuvent être chargés dans l'iframe.
Exemple : Empêcher les scripts non sécurisés dans votre iframe :
Content-Security-Policy: script-src 'self' https://example.com;
1.3 Problèmes liés aux Cross-Origin Policy
Problème : Les iframes affichant des contenus provenant d'autres domaines peuvent causer des problèmes de sécurité et de confidentialité. Pour cela les navigateurs appliquent une certaine politique pour limiter les interactions entre deux origines différentes.
Exemple simple :
- Domaine A :
https://interne-app.com
- Domaine B :
https://externe-app.com
Un script sur https://interne-app.com
ne peut pas lire ou modifier directement le contenu de https://externe-app.com
dans l'iframe.
Solution
La communication entre ces deux domaines doit se faire via des méthodes contrôlées comme PostMessage (voir section suivante).
1.4 Isolation des ressources
Problème : Si l'iframe partage des ressources, telles que des cookies ou du stockage local, avec le domaine principal, cela peut entraîner des fuites de données ou des attaques comme le session fixation.
Utilisation de cookies sécurisés
Solution :Configurez les cookies avec les attributs suivants :
-
Secure
: Un cookie sécurisé est envoyé uniquement si la requête est faite en https. -
HttpOnly
: Empêche JavaScript d'accéder au cookie. -
SameSite
: Contrôle si un cookie est envoyé avec les requêtes d'origine croisée :-
Strict
: Le navigateur envoie le cookie uniquement pour les demandes provenant du même site. -
Lax
: Le cookie n'est pas envoyé sur les requêtes inter-sites, telles que les appels pour charger des images ou des iframes, mais il est envoyé lorsqu'un utilisateur navigue vers le site d'origine à partir d'un site externe.
-
allow
dans l'iframe
Solution : Attribut L'attribut allow
spécifie les permissions (voir Feature-Policy/Permissions-Policy) de l'iframe pour accéder aux API sensibles.
Exemple :
<iframe src="https://example.com" allow="fullscreen"></iframe>
2. Communication entre iframes et parent
Pour permettre la communication entre les iframes ou entre l'iframe et son parent, les navigateurs modernes fournissent plusieurs méthodes. Les plus courantes d'utilisations étant PostMessage
et BroadcastChannel
.
PostMessage
2.1Concept du PostMessage
PostMessage
permet une communication sécurisée entre deux contextes (window ou iframes) même s'ils appartiennent à des domaines différents. C'est une solution idéale pour surmonter les "restrictions" (ex: SOP) tout en limitant les risques de sécurité.
Exemple technique
Imaginons une application contenant une iframe (https://app.example.com
) chargé dans une page principale (https://parent.example.com
). Le parent veut envoyer un message à l'iframe.
Étape 1 : Envoi de message depuis le parent
const iframe = document.getElementById('iframe');
iframe.contentWindow.postMessage(
{ type: 'INIT', payload: 'Hello from parent!' },
'https://app.example.com'
);
Étape 2 : Réception du message dans l'iframe
window.addEventListener('message', (event) => {
if (event.origin !== 'https://parent.example.com') {
console.warn('Origine non autorisée:', event.origin);
return;
}
console.log('Message reçu:', event.data);
});
Retour de message à la fenêtre parent
window.parent.postMessage(
{ type: 'RESPONSE', payload: 'Hello parent!' },
'https://parent.example.com'
);
Sécuriser PostMessage
Bien qu'PostMessage
soit bien utile, il s'expose à des risques si les messages ne sont pas validés correctement.
event.origin
)
Vérification de l'origine (Chaque message contient un champ origin
indiquant la source. Vérifiez cette origine avant de traiter le message est primordial.
if (event.origin !== 'https://trusted-domain.com') {
console.warn('Origine non autorisée:', event.origin);
return;
}
Validation des données
Utilisez des schémas ou des vérifications pour vous assurer que le contenu du message correspond aux attentes. C'est une bonne pratique, essayons de la respecter.
if (typeof event.data !== 'object' || !event.data.type) {
console.error('Message invalide:', event.data);
return;
}
BroadcastChannel
2.2Concept du BroadcastChannel
L'interface BroadcastChannel représente un canal nommé auquel peut s'abonner n'importe quel contexte de navigation d'une même origine. Il permet donc une communication en temps réel entre différentes fenêtres, onglets, cadres ou iframes partageant la même origin.
Exemple
Imaginons une situation où plusieurs iframes doivent rester synchronisées sur une même session utilisateur.
Création d'un canal
Chaque contexte (iframe ou parent) crée un canal avec le même nom. Ici le canal s'appelle app-sync
.
const channel = new BroadcastChannel('app-sync');
Envoi d'un message
Depuis n'importe quel contexte, on envoie un message sur le canal :
channel.postMessage({ type: 'SYNC', data: { theme: 'dark' } });
Réception des messages
Tous les participants du canal peuvent écouter les messages. Ici, pour l'exemple, on applique un thème synchronisé.
channel.onmessage = (event) => {
console.log('Message reçu:', event.data);
if (event.data.type === 'SYNC') {
applyTheme(event.data.data.theme);
}
};
Comparaison PostMessage vs BroadcastChannel
Aspect | PostMessage | BroadcastChannel |
---|---|---|
Origines | Fonctionne entre origines différentes | Doit être utilisé sur une même origine |
Configuration | Nécessite la validation manuelle de l'origine | Pas de validation nécessaire |
Cas d'usage | Communication entre parent et iframe | Synchronisation multi-contextes |
2.3 Web Messaging Patterns
Lorsqu'une application contient plusieurs iframes ou utilise à la fois PostMessage
et BroadcastChannel
, des modèles de communication organisés sont nécessaires.
Pattern 1 : Message Dispatcher
Un parent agit comme hub central pour router les messages entre différentes iframes.
Architecture simple
- Les iframes n'envoient des messages qu'au parent.
- Le parent route les messages vers les autres iframes.
Exemple : Dispatcher central
Dans le parent :
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-domain.com') return;
// Message venant de iframe1 pour iframe2
if (event.data.target === 'iframe2') {
const iframe2 = document.getElementById('iframe2');
iframe2.contentWindow.postMessage(event.data, 'https://iframe2.example.com');
}
});
Dans iframe1 :
window.parent.postMessage(
{ target: 'iframe2', type: 'DATA_UPDATE', payload: { key: 'value' } },
'https://parent.example.com'
);
Pattern 2 : Événements Pub/Sub avec BroadcastChannel
BroadcastChannel peut être utilisé pour un modèle pub/sub où chaque contexte s'abonne à des types d'événements spécifiques.
Architecture tout aussi simple
- Une iframe publie un thème.
- Tous les participants appliquent le thème.
Exemple : Synchronisation des thèmes
Iframe 1 => Publication :
channel.postMessage({ type: 'THEME_CHANGE', payload: { theme: 'dark' } });
Iframe 2 => Abonnement :
channel.onmessage = (event) => {
if (event.data.type === 'THEME_CHANGE') {
applyTheme(event.data.payload.theme);
}
};
2.4 Messages Chiffrés
Dans des cas que je n'ai pas encore vu, mais comme je viens de découvrir la chose, vous pouvez chiffrer les messages échangés via PostMessage
ou BroadcastChannel
.
Exemple simple avec AES (Crypto API)
Voir Crypto API pour plus d'informations, ainsi que AES-GCM pour les paramètres. (J'ai pas tout assimilé, mais ça a l'air cool)
Fonction de chiffrement :
async function encryptMessage(message, key) {
const encodedMessage = new TextEncoder().encode(message);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: window.crypto.getRandomValues(new Uint8Array(12)) },
key,
encodedMessage
);
return encrypted;
}
Fonction de déchiffrement :
async function decryptMessage(encryptedMessage, key) {
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: window.crypto.getRandomValues(new Uint8Array(12)) },
key,
encryptedMessage
);
return new TextDecoder().decode(decrypted);
}
Exemple via docker compose
Vous pouvez tester les exemples situés dans le dossier examples
en utilisant docker compose up
.
Vous devez avant cela ajouter dans votre etc/hosts les lignes suivantes :
127.0.0.1 broadcast.localhost
127.0.0.1 broadcast-iframe.localhost
127.0.0.1 postmessage.localhost
127.0.0.1 postmessage-iframe.localhost
127.0.0.1 csrf-protected.localhost
127.0.0.1 csrf-attacker.localhost
127.0.0.1 xss-protected.localhost
127.0.0.1 xss-vulnerable.localhost
127.0.0.1 csrf-vulnerable.localhost
127.0.0.1 xss-attacker.localhost