[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"doc:fr:plugins\u002Fsecurity":3},{"title":4,"lede":5,"html":6,"headings":7},"La sécurité des plugins","Un plugin du store passe par trois gardes : les onze contrôles du store à la soumission, la revue « Vérifié par Escouade » faite à la main, et les vérifications de l’app à chaque installation. Cette page dit ce que chacune tient, ce que la revue refuse, et ce que le sceau ne promet pas.","\u003Ch2 id=\"ce-que-l-app-verifie\">Ce que l’app vérifie\u003C\u002Fh2>\n\u003Cp>À l’installation et à chaque mise à jour, Escouade télécharge \u003Ccode class=\"key\">version.json\u003C\u002Fcode>, l’archive et son module WASM, puis vérifie :\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>les empreintes\u003C\u002Fstrong> : \u003Ccode class=\"key\">version.json\u003C\u002Fcode> contre le catalogue, l’archive, le module et le manifeste contre \u003Ccode class=\"key\">version.json\u003C\u002Fcode> (SHA-256). Un octet changé, sur le store, en route ou dans un cache, arrête l’installation ;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>la signature\u003C\u002Fstrong>, pour une version vérifiée : \u003Ccode class=\"key\">version.json\u003C\u002Fcode> signé par minisign avec la clé d’Escouade, et le commentaire de la signature qui nomme cet identifiant et ce numéro. Les clés publiques sont dans l’app ; la clé secrète vit sur un poste, chiffrée, jamais sur le serveur du store, jamais dans la CI ni dans un dépôt. Le store ne peut donc pas fabriquer un sceau ;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>l’archive\u003C\u002Fstrong> lue en mémoire, sans lien symbolique ni chemin qui sorte du dossier du plugin ;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>le dossier\u003C\u002Fstrong> que \u003Ccode class=\"key\">version.json\u003C\u002Fcode> nomme pour un plugin rangé dans un sous-dossier de son dépôt : un chemin relatif simple, sans \u003Ccode class=\"key\">..\u003C\u002Fcode>, ni chemin absolu, ni antislash ; sinon rien n’est installé. Le store, lui, refuse à la soumission un dossier qui passe par un lien symbolique.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Le sceau d’un plugin installé est revérifié sur ta machine, sans le store. Puis la fenêtre d’installation montre ses permissions en mots, avant que rien ne tourne (\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Finstaller\u002F\">installer un plugin\u003C\u002Fa>).\u003C\u002Fp>\n\u003Ch2 id=\"ce-que-le-sceau-couvre\">Ce que le sceau couvre\u003C\u002Fh2>\n\u003Cp>« Vérifié par Escouade » vaut pour \u003Cstrong>une version\u003C\u002Fstrong> : son code au commit publié, son archive et son module, recompilé par la revue, désignés par l’empreinte de son \u003Ccode class=\"key\">version.json\u003C\u002Fcode>. Une nouvelle version est « non vérifiée » jusqu’à sa propre revue, même si la précédente l’était : c’est dans une mise à jour qu’un plugin honnête peut changer de mains. \u003Cstrong>N’installer que des versions vérifiées\u003C\u002Fstrong>, dans la vue Plugins, refuse toute version sans sceau.\u003C\u002Fp>\n\u003Cp>Le sceau dit qu’une personne a lu ce code au regard de ce qu’il déclare, et que le banc l’a fait tourner sans réseau. Il ne promet pas l’absence de bogue, ni la qualité du service tiers que le plugin joint.\u003C\u002Fp>\n\u003Ch2 id=\"ce-que-la-revue-refuse\">Ce que la revue refuse\u003C\u002Fh2>\n\u003Cp>La revue lit chaque version avec un script qui lui prépare des pistes, fichier et ligne, et un agent qui lit le code comme des données. Ses issues : « Vérifier », « Demander des changements » (la version reste publiée, non vérifiée, le message dit quoi corriger) ou « Refuser ».\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Refusé d’office\u003C\u002Fstrong>, sans lire plus loin :\u003C\u002Fp>\n\u003Cul>\n\u003Cli>une archive ou un module dont l’empreinte n’est pas celle de \u003Ccode class=\"key\">version.json\u003C\u002Fcode> ;\u003C\u002Fli>\n\u003Cli>une version qui n’est plus celle que le store a figée.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>Pas de sceau tant que ce n’est pas corrigé\u003C\u002Fstrong> :\u003C\u002Fp>\n\u003Cul>\n\u003Cli>un hôte joint qui n’est pas dans \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode> (une adresse en dur, un \u003Ccode class=\"key\">fetch\u003C\u002Fcode>, un \u003Ccode class=\"key\">WebSocket\u003C\u002Fcode>) ;\u003C\u002Fli>\n\u003Cli>la lecture de secrets ou de dossiers personnels hors de ce que le plugin déclare : variables d’environnement, \u003Ccode class=\"key\">~\u002F.ssh\u003C\u002Fcode>, \u003Ccode class=\"key\">~\u002F.claude\u003C\u002Fcode>, le trousseau hors de \u003Ccode class=\"key\">secrets\u002Fget\u003C\u002Fcode> ;\u003C\u002Fli>\n\u003Cli>des fichiers lus ou écrits hors du dossier du plugin et de ce que ses permissions ouvrent ;\u003C\u002Fli>\n\u003Cli>un programme lancé que les permissions n’annoncent pas (\u003Ccode class=\"key\">child_process\u003C\u002Fcode>, \u003Ccode class=\"key\">spawn\u003C\u002Fcode>, \u003Ccode class=\"key\">exec\u003C\u002Fcode>) ;\u003C\u002Fli>\n\u003Cli>du code dynamique : \u003Ccode class=\"key\">eval\u003C\u002Fcode>, \u003Ccode>new Function\u003C\u002Fcode>, un \u003Ccode class=\"key\">import()\u003C\u002Fcode> d’une adresse ;\u003C\u002Fli>\n\u003Cli>du code illisible : minifié sans sa source, du texte caché en base64, des blobs binaires ;\u003C\u002Fli>\n\u003Cli>des dépendances non épinglées, un script \u003Ccode class=\"key\">postinstall\u003C\u002Fcode>, une dépendance tirée d’un dépôt git ;\u003C\u002Fli>\n\u003Cli>des consignes ajoutées aux agents qui les détournent de leur tâche (une injection de prompt) ;\u003C\u002Fli>\n\u003Cli>un module que la recompilation ne redonne pas, à l’octet près ;\u003C\u002Fli>\n\u003Cli>une dépendance Rust qu’on ne peut pas justifier : ce que \u003Ccode class=\"key\">Cargo.lock\u003C\u002Fcode> ajoute est du code qui tourne ; du code \u003Ccode class=\"key\">unsafe\u003C\u002Fcode>, un \u003Ccode class=\"key\">extern\u003C\u002Fcode> ou un \u003Ccode class=\"key\">#[link]\u003C\u002Fcode> sans raison ;\u003C\u002Fli>\n\u003Cli>\u003Ccode class=\"key\">agentHome\u003C\u002Fcode>, ou une entrée en \u003Ccode class=\"key\">*\u003C\u002Fcode> de \u003Ccode class=\"key\">permissions.commands\u003C\u002Fcode>, sans une raison relue ;\u003C\u002Fli>\n\u003Cli>une étiquette déplacée depuis la soumission, une entrée d’archive refusée (lien, \u003Ccode class=\"key\">..\u003C\u002Fcode>, chemin absolu) ;\u003C\u002Fli>\n\u003Cli>un banc de conformité qui échoue, ou qui tente le réseau ou une écriture que le bac à sable refuse.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>Retiré\u003C\u002Fstrong> : un comportement malveillant découvert (envoyer des données ailleurs, cacher ce que fait le code, mentir sur ses permissions) mène au retrait de la version, et du plugin s’il le faut.\u003C\u002Fp>\n\u003Ch2 id=\"le-retrait-et-la-liste-de-revocation\">Le retrait et la liste de révocation\u003C\u002Fh2>\n\u003Cp>Escouade lit une \u003Cstrong>liste de révocation\u003C\u002Fstrong> signée par la même clé, au démarrage puis toutes les 6 heures, et refuse une liste plus ancienne que la dernière lue. Elle a deux sortes d’entrées :\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>retirer le sceau\u003C\u002Fstrong> : le plugin continue, une bande dit pourquoi ;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>retrait d’urgence\u003C\u002Fstrong> : l’app arrête le plugin, le désactive et dit pourquoi, avec « Désinstaller ». Elle ne supprime rien seule : tes données sont à toi.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>La liste est aussi copiée sur escouade.dev, une heure au plus après le store : si le store tombe, l’app la lit là. La dernière liste lue s’applique hors ligne.\u003C\u002Fp>\n\u003Ch2 id=\"signaler-un-plugin\">Signaler un plugin\u003C\u002Fh2>\n\u003Cp>« Signaler », sur la page d’un plugin du store ou dans l’app, est ouvert à tous avec une adresse e-mail, sans compte GitHub : malveillant, ne fait pas ce qu’il dit, permissions trompeuses, licence, autre. Une personne lit chaque signalement et te répond. L’auteur voit qu’un signalement est ouvert, jamais qui l’a fait.\u003C\u002Fp>\n\u003Ch2 id=\"un-plugin-hors-du-store\">Un plugin hors du store\u003C\u002Fh2>\n\u003Cp>Un plugin installé depuis un dossier ou un dépôt git n’a passé ni les contrôles ni la revue : il est \u003Cstrong>non vérifié\u003C\u002Fstrong>, et la fenêtre d’installation le dit en tête. Ses permissions s’appliquent de la même façon.\u003C\u002Fp>\n",[8,12,15,18,21,24],{"id":9,"text":10,"depth":11},"ce-que-l-app-verifie","Ce que l’app vérifie",2,{"id":13,"text":14,"depth":11},"ce-que-le-sceau-couvre","Ce que le sceau couvre",{"id":16,"text":17,"depth":11},"ce-que-la-revue-refuse","Ce que la revue refuse",{"id":19,"text":20,"depth":11},"le-retrait-et-la-liste-de-revocation","Le retrait et la liste de révocation",{"id":22,"text":23,"depth":11},"signaler-un-plugin","Signaler un plugin",{"id":25,"text":26,"depth":11},"un-plugin-hors-du-store","Un plugin hors du store",1793889379916]