On n’a pas droit à une deuxième chance pour un écosystème de plugins. La première fois qu’une extension lit les données du mauvais tenant ou écrit dans votre localStorage, vous vous offrez un trimestre perdu, un cycle de communication de crise et un déficit de confiance que vous rembourserez pendant des années. Un billet de dev récent, « Web Components vs. Iframes: A Hard Lesson in DOM Isolation Barriers », a circulé parce qu’il expose une vérité discrète : le Shadow DOM n’est pas une frontière de sécurité. Si vous construisez une marketplace ou une surface « extensions » en 2026, traitez la composition UI et l’isolation de sécurité comme deux décisions distinctes — parce que le navigateur le fait.
Le vrai problème à résoudre
« Laisser des tiers étendre notre app » semble un objectif unique, mais c’est en réalité quatre problèmes aux solutions incompatibles :
- Exécution de code non approuvé : un plugin peut-il exécuter du JS arbitraire sans exfiltrer des données ni casser l’hôte ?
- Intégration au DOM : un plugin peut-il dessiner dans votre UI sans faire imploser votre design system ?
- Périmètre des données et des capacités : un plugin ne peut-il faire que ce que l’utilisateur et le tenant autorisent — rien de plus ?
- Isolation des performances : pouvez-vous empêcher un mauvais plugin de plomber l’INP, le TTI ou la mémoire pour tout le monde ?
Si vous mélangez tout, vous choisirez le mauvais primitif. C’est ainsi que des équipes finissent par confier aux Web Components un travail de sécurité qu’ils ne peuvent pas faire, ou par gonfler la page avec des iframes qui n’étaient pas nécessaires.
La réponse courte
- Si vous comptez un jour autoriser des développeurs tiers que vous n’employez pas directement, utilisez des iframes cross-origin pour isoler l’exécution. Point final.
- Utilisez les Web Components (Shadow DOM) pour des briques UI first-party ou de partenaires triés sur le volet, là où vous avez besoin d’une forte intégration DOM — pas pour du sandboxing.
- Transférez la logique de plugin lourde ou longue dans des Web Workers et communiquez via des canaux de messages. Les Workers ne constituent pas une frontière de sécurité, mais ils préservent le main thread.
- Encapsulez tout dans des APIs hôte à capacités restreintes. Ne passez jamais des handles bruts de window, document ou storage au code du plugin.
C’est la colonne vertébrale. Les détails ci-dessous la rendent digne de la production.
Pourquoi les Web Components ne sont pas votre sandbox
Le Shadow DOM et les custom elements règlent les fuites de CSS et d’événements. Ils ne résolvent pas la protection des données cross-tenant. Un plugin malveillant ou simplement bogué dans un Web Component peut :
- Lire tout état accessible en JS que vous lui passez (props, context, tokens).
- Appeler fetch avec vos identifiants ambiants si vous lui donnez un accès same-origin à vos APIs.
- Exfiltrer via tout callback hôte ouvert qu’il reçoit.
Oui, vous pouvez durcir avec Trusted Types, une CSP stricte et des frontières de types rigoureuses. Mais si le développeur est en dehors de votre organisation — et que votre modèle de menace inclut « le plugin tente d’en faire plus que prévu » — les Web Components servent à la composition, pas au confinement.
Pourquoi les iframes cross-origin sont le choix par défaut, ennuyeux mais correct
Les iframes, c’est de la sécurité à bas coût. Le navigateur les a blindées depuis deux décennies. En 2026, les primitives d’isolation sur lesquelles vous pouvez compter :
- Frontière d’origine : Origine différente, identifiants ambiants différents. La dépréciation des cookies tiers vous est favorable ici.
- Attribut sandbox : Retirez
allow-same-originet l’iframe devient une origine opaque. N’accordez que le nécessaire :allow-scripts,allow-forms, éventuellementallow-popups-to-escape-sandboxpour OAuth. Désactivez micro, caméra, USB, paiements par défaut. - Permissions Policy : Refusez les APIs puissantes par iframe. Pas de geolocation. Pas de window-management. Pas d’idle-detector. Refus par défaut de tout.
- RPC via postMessage :
MessageChannelisole les communications sur des ports explicites. Des bibliothèques comme Comlink rendent cela ergonomique sans fuite de références.
Les compromis sont réels. Chaque iframe coûte de la mémoire (quelques Mo sont courants), de la complexité de layout et une couche d’intégration plus fine. Mais quand un plugin vient de « quelqu’un sur Internet », c’est la différence entre un rapport d’incident et un billet de blog dont vous êtes fier.
Un cadre de décision pragmatique
1) Qui écrit le code et qui sera tenu responsable en cas de problème
- Uniquement des équipes first-party : Utilisez des Web Components pour l’ergonomie et le Shadow DOM pour l’isolation des styles. Contrôlez malgré tout l’accès via des APIs hôte étroites. Pas d’état global ambiant.
- Partenaires stratégiques sous contrat : Démarrez avec des iframes pour l’isolation, puis exposez sélectivement des surfaces « façon DOM » via des APIs hôte. Si vous devez absolument utiliser des Web Components, encapsulez le code partenaire dans un Worker et médiez tous les appels hôte via un
MessageChannel. - Marketplace ouverte : Iframes cross-origin avec sandbox et Permissions Policy. Sans exception.
2) Ce que le plugin doit faire
- Widgets UI en lecture seule (tables, graphiques, panneaux) : Les Web Components conviennent pour du first-party. Pour du tiers, rendez dans une iframe ; passez des instantanés de données, pas des handles.
- Workflows mutatifs (create/update/delete dans votre app) : Encadrez toujours par des capacités via des jetons d’action signés, émis côté serveur par couple tenant-ressource-opération avec TTL de 5–15 minutes. Pas de bearer tokens à autorité globale.
- Calcul lourd (parsing, transformations IA) : Déportez vers un Worker dans l’origine de l’iframe ou vers votre runtime d’extensions côté serveur. Évitez les longues tâches sur le main thread : budget de 50 ms max par interaction ; ralentissez ou rendez la main avec
scheduler.postTask({priority: "background"}). - Accès réseau : Préférez une API fetch proxifiée par l’hôte qui impose CORS, journalise les requêtes, retire les cookies et applique des limites de débit. Ne donnez pas d’accès réseau brut aux plugins non approuvés.
3) Le niveau d’intégration visuelle nécessaire
- « On dirait une partie de l’app » : Pour les iframes, fournissez un pont de design tokens : exposez des variables CSS et une palette clair/sombre via un message de handshake au chargement de l’iframe. Ne partagez pas tout votre runtime de design system ; envoyez des tokens résolus.
- Événements, focus, ARIA : Pour l’accessibilité, proxiez les événements hôte de haut niveau (resize, changement de thème, changement de locale) via un
MessagePortdédié. À l’intérieur de l’iframe, le plugin possède la sémantique ARIA. Vous ne pouvez pas garantir l’accessibilité cross-frame aussi finement que dans des arbres shadow ; fixez cette attente. - Drag & drop, sélection, overlays/portals : Ce sont des points douloureux. Fournissez des primitives gérées par l’hôte : API de portail d’overlay détenue par l’hôte ; coordination de drag/drop détenue par l’hôte qui envoie des intentions aux plugins plutôt que de leur donner des écouteurs globaux.
Contrôles de sécurité à mettre en place dès le premier jour
- CSP sur les origines hôte et plugin : Hôte :
script-src 'self'+ hachages pour vos scripts, pas de'unsafe-inline'. Origine plugin : CSP séparée sans exécution de code distante au-delà du bundle. Pour les chargements de Web Components, épinglez avec SRI et des versions exactes. - Baseline Permissions Policy : Tout refuser par défaut :
geolocation=(),camera=(),microphone=(),payment=(),usb=(),battery=(),clipboard-write=()sauf besoin produit concret. - Contrat sandbox de l’iframe :
sandbox="allow-scripts allow-forms"au minimum ; évitezallow-same-origin. Ajoutezallow-popupsetallow-popups-to-escape-sandboxuniquement pour les flux OAuth ; terminez les canaux de messages après la fin de l’auth. - Jetons de capacités, pas de cookies de session : Émettez des JWT étroits : {tenant, resource, operation, plugin_id, exp ≤ 15m}. Liez-les à l’iframe via l’initialisation par message-port ; n’exposez jamais de tokens de session globaux au JS du plugin.
- Rate limiting et journalisation d’égarement par plugin_id : Chaque appel relayé par l’hôte est mesuré et tagué avec les identifiants tenant et plugin. Vous en aurez besoin quand la Finance demandera pourquoi un plugin a explosé l’égarement de 3 To.
- Trusted Types + DOMPurify (strict) : Pour tout rendu HTML que l’hôte effectue au nom d’un plugin, exigez du TrustedHTML. Traitez le HTML cross-frame comme hostile jusqu’à sa désinfection.
Budgets de performance et d’UX que vous pouvez faire respecter
- Mémoire : Attendez-vous à 3–8 Mo de base par iframe dans Chromium moderne. Sur un dashboard avec 6 plugins, vous venez de dépenser ~30–50 Mo. Fixez une limite dure : maximum 4 iframes tierces visibles en concurrence ; lazy-load le reste.
- Latence d’interaction (INP) : Budget ≤ 200 ms p95. Tout plugin générant des longues tâches > 50 ms plus de 5 fois en une minute est ralenti via votre ordonnanceur hôte et signalé dans la marketplace.
- Réseau : Limitez les requêtes sortantes des plugins à 10 RPS par tenant par défaut, avec burst ≤ 50. Tout ce qui touche à l’IA doit passer sur votre runtime d’extensions côté serveur pour garder la dépense de tokens et la latence prédictibles.
- Cold start : Les iframes démarrent plus lentement. Pré-initialisez des iframes cachées avec
loading="lazy"et un handshake explicite pendant les fenêtres d’inactivité. Prévoyez 80–200 ms de TTI supplémentaire vs du code en page, selon la taille du bundle.
L’expérience développeur que vous devez à votre écosystème
La sécurité sans DX meurt au contact du réel. Offrez aux développeurs de plugins un contrat facile à suivre et difficile à mal utiliser :
- Manifest + capabilities : Un manifest JSON déclarant routes, points de montage UI et capacités demandées. Rejetez à l’enregistrement si cela dépasse ce qui est autorisé pour sa catégorie.
- API hôte typée : Livrez un SDK TypeScript qui encapsule postMessage avec Comlink, impose les périmètres de capacités au compile-time et fournit des mocks pour le dev local.
- Design tokens, pas votre CSS : Exportez des tokens pour couleur, espacements et typographie via un schéma JSON stable ; les plugins mappent leurs propres composants. Ne couplez pas votre cadence de release au CSS des tiers.
- Conteneur de dev local : Une CLI qui lance un hôte iframe en localhost avec le même sandbox et les mêmes policies qu’en prod. Zéro dérive « ça marche en local ».
- Observabilité : Fournissez une console plugin : logs, appels réseau, hits de rate limit, refus de capacités. Exposez le p95 d’initialisation et l’INP aux développeurs pour qu’ils puissent optimiser.
Une politique de marketplace qui déplace réellement la responsabilité
Si vous laissez du code externe s’exécuter contre les données de vos clients, opérez comme une plateforme :
- Niveaux de revue de sécurité : Bronze (vérifications automatiques uniquement), Silver (revue humaine + scan de code + test d’intrusion), Gold (re-cert annuelle, rapport SOC 2). Ajustez votre partage de revenus en conséquence.
- Key escrow pour takedowns : Chaque release de plugin doit être adressable : bundle signé avec métadonnées de build reproductible. Si vous devez révoquer, vous le pouvez. Utilisez Subresource Integrity pour la livraison de scripts quand c’est possible.
- Playbook d’incident : Désactivation en un clic par tenant et kill switch global. Exigez que les plugins implémentent une route « safe mode » pour l’export de données et la désinstallation.
- Empreinte de données claire : Documentez publiquement ce qu’un plugin peut faire et quelles capacités il utilise. Rendez cela recherchable pour que les acheteurs filtrent selon leur tolérance au risque.
Extensions côté serveur : n’imposez pas tout au navigateur
Beaucoup de « nous avons besoin d’un accès DOM profond » est auto-infligé. Scindez le monde :
- Surfaces visuelles côté client : Utilisez des iframes ou des Web Components selon les règles ci-dessus.
- Actions et automatisations côté serveur : Exécutez-les dans votre runtime d’extension (Node, Deno ou WASM) avec des frontières strictes de tenancy, un égarement audité et des requêtes signées vers vos APIs cœur. L’UI du navigateur ne fait que déclencher des intentions.
Agir ainsi réduit drastiquement la pression pour sur-permissionner le plugin navigateur juste pour atteindre des données ou des GPUs qu’il ne devrait jamais toucher.
Nouveautés 2026 à surveiller
- Dépréciation des cookies tiers : C’est à votre avantage. Les iframes cross-origin n’hériteront pas de votre session. Concevez des capacités à base de tokens dès le premier jour.
- Fenced Frames : Idéales pour du contenu non approuvé de type publicité avec garanties de confidentialité, mais encore trop contraignantes pour la plupart des plugins d’app. Suivez le support ; n’en faites pas encore le socle de votre plateforme.
- Base UI, churn shadcn/ui : Les migrations récentes de bibliothèques de composants vous rappellent ceci : les design systems changent ; les frontières de sécurité non. Gardez la sécurité orthogonale à votre stack UI.
Une architecture de référence à copier
- Service de registre : Stocke les manifests de plugins, les métadonnées de bundle signé et les capacités. Fait respecter les politiques par catégorie au moment de la publication.
- Shell hôte (app) : Rend les points de montage des plugins. Pour les plugins non approuvés, injecte des iframes cross-origin avec des attributs
sandboxetallowselon les capacités. Initialise unMessageChannelpar plugin. - SDK hôte : Wrapper type-safe autour de postMessage/Comlink. Expose des APIs :
data.read(resource),actions.execute(actionToken),ui.openOverlay(),storage.session.get/set(portée à plugin_id+tenant). - Passerelle de politique : Service côté serveur qui émet des jetons de capacité de courte durée et proxifie l’égarement réseau des plugins avec logs et limites de débit. Lie les tokens à tenant+plugin_id+operation.
- Runtime d’extensions côté serveur : Exécute les étapes lourdes ou sensibles sous votre contrôle. Communique avec les services cœur via mTLS et requêtes signées. Expose des webhooks pour que l’iframe du plugin reçoive des mises à jour.
- Plan d’observabilité : Tableaux de bord par plugin : temps d’init, INP, taux d’erreur, égarement, refus de capacités. Signalement automatique des extrêmes dans la marketplace.
- Kill switch et feature flags : Toggles centralisés pour révoquer instantanément une version ou une capacité à travers les tenants.
Pièges courants (et le correctif)
- Piège : Partager votre store Redux ou le contexte React avec un plugin Web Component. Correctif : Exposez des instantanés en lecture seule via structured clone ; toutes les écritures passent par des actions hôte gardées par capacités.
- Piège : Ajouter
allow-same-originà l’iframe parce qu’un dev avait besoin de cookies. Correctif : N’y touchez pas. Proxyez l’auth via l’hôte ; si vous y êtes forcé, isolez ce flux dans une iframe d’auth dédiée avec un canal mis en quarantaine et détruisez-la après usage. - Piège : Laisser les plugins enregistrer des écouteurs d’événements globaux. Correctif : Fournissez des APIs d’événements détenues par l’hôte, à portée limitée, et throttlez-les.
- Piège : La dérive de version de votre design system casse les plugins. Correctif : Versionnez vos design tokens et supportez au moins deux versions majeures en parallèle. Le contrat plugin, ce sont les tokens, pas les class names.
Pourquoi c’est crucial pour les apps très orientées IA
Les intégrations d’agents amplifient le risque. Un plugin qui peut tout voir finira par se faire injecter un prompt qui le pousse à tout faire. Gardez les contextes de modèles étroits : lorsqu’un plugin demande de l’aide IA, l’hôte assemble des tranches de contexte, expurge les secrets et signe la requête pour le modèle. Aucun plugin ne devrait jamais détenir de clés d’API brutes ni d’index globaux par tenant. Attendez-vous à ce que les régulateurs s’en mêlent — bientôt.
Ce que nous faisons pour nos clients chez DHD Tech
Nous avons livré des plateformes de plugins pour des équipes SaaS américaines où l’écart entre « belle app » et « plateforme défendable » se compte en semaines, pas en trimestres. Notre stack par défaut est :
- Iframes cross-origin pour le code tiers avec sandbox et Permissions Policy strictes.
- APIs hôte typées au-dessus de Comlink, avec des JWT par tenant et par capacité expirant en 5–15 minutes.
- Ponts de design tokens pour la cohésion visuelle sans enchevêtrement CSS.
- Runtimes d’extensions côté serveur pour les charges lourdes/réglementées, avec égarement budgété et suivi des tokens.
- Tests de conformité automatisés qui lancent 50+ plugins synthétiques en CI pour valider l’isolation, les limites de débit et les budgets de performance sous charge.
C’est volontairement ennuyeux. L’alternative, c’est se réveiller face à un fil de discussion sur une brèche affirmant que les « vidéos privées » de quelqu’un ont fuité à cause de votre bug cross-tenant. Ne soyez pas ce billet.
À retenir
- Utilisez des iframes cross-origin pour tout code que vous ne contrôlez pas ; les Web Components servent à la composition, pas au confinement.
- Exposez des APIs hôte étroites et à capacités ; ne passez jamais de DOM brut, de storage ou de tokens globaux aux plugins.
- Faites respecter des budgets : mémoire par iframe, INP p95, RPS réseau et kill switches pour les mauvais acteurs.
- Les design tokens assurent l’intégration visuelle ; ne partagez pas le runtime de votre design system.
- Séparez l’UI client des extensions côté serveur ; gardez le sensible et le lourd hors du navigateur.
- Investissez dans un manifest, un SDK TypeScript, un hôte local et de l’observabilité pour que le chemin sûr soit le plus simple.