Skip to content
Snippets Groups Projects
Theo LECOUBLET's avatar
Theo LECOUBLET authored
4dc9ed2b
History

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

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.

Solution : CSP avec frame-ancestors

(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.

Solution 1 : Attribut sandbox

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 et allow-same-origin ne sont pas spécifiés)
  • Les fenêtres contextuelles.

Personnalisation des permissions avec sandbox

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>

Solution 2 : Content-Security-Policy (CSP)

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.

Solution : Utilisation de cookies sécurisés

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.

Solution : Attribut allow dans l'iframe

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.

2.1 PostMessage

Concept 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.

Vérification de l'origine (event.origin)

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;
}

2.2 BroadcastChannel

Concept 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.

Schéma du pattern Message Dispatcher

Architecture simple
  1. Les iframes n'envoient des messages qu'au parent.
  2. 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.

Schéma du pattern Pub/Sub

Architecture tout aussi simple
  1. Une iframe publie un thème.
  2. 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