CVE-2026-35211
CVSS 6.5 · Moyenne CorrigéCVE-2026-35211 : injection de scripts Painless dans OpenCTI via l’opérateur de filtre GraphQL script
Tout utilisateur OpenCTI doté de la capacité KNOWLEDGE pouvait envoyer des scripts Painless bruts à Elasticsearch via un opérateur de filtre public.
Pour décider
- Impact
- Un filtre peut faire exécuter un script Painless par Elasticsearch et provoquer une consommation excessive de ressources.
- Conditions
- Un compte OpenCTI doté de la capacité KNOWLEDGE est requis. Une indisponibilité effective n’a pas été mesurée dans notre test.
- Action recommandée
- Installer OpenCTI 7.260401.0 ou ultérieur et conserver l’opérateur script désactivé sauf besoin explicitement validé.
Fiche de l’avis
- Produit
- OpenCTI
- Paquet
- npm · opencti-graphql
- Versions affectées
- < 7.260401.0
- Corrigé dans
- 7.260401.0
- CVSS
- 6.5 Moyenne
- Accès requis
- Compte requis
- Avis publié
- Crédit
- rootzero
Synthèse de l’enquête
L’API GraphQL d’OpenCTI exposait une valeur script dans son enum FilterOperator, et le constructeur de requêtes recopiait la valeur du filtre dans une requête script Elasticsearch sans aucun contrôle. Tout utilisateur authentifié doté de la capacité KNOWLEDGE pouvait faire exécuter du code Painless arbitraire au cluster de recherche sur chaque document candidat, de quoi épuiser son CPU et dégrader la plateforme pour tous. Corrigé en 7.260401.0, où l’opérateur est refusé sauf activation explicite.
L’opérateur GraphQL qui exécute un script
Nous sommes partis du schéma GraphQL d’OpenCTI. Parmi les opérateurs de filtre qui décrivent des comparaisons, script détonnait : il désignait un mécanisme d’exécution. Nous avons suivi sa valeur dans le constructeur des filtres Elasticsearch et constaté qu’un utilisateur autorisé à interroger des objets pouvait lui transmettre du code Painless. La question n’était pas de savoir si un script sortait du bac à sable Elasticsearch, mais s’il était raisonnable de laisser un filtre utilisateur en créer un.
Nous avons écrit une requête GraphQL portant une expression Painless témoin, puis suivi le constructeur de filtres pour voir où elle serait insérée. Le rapport proposait aussi une boucle coûteuse comme essai de disponibilité ; il ne conserve ni indisponibilité mesurée ni réponse d’un cluster réel. Nous avons donc borné l’impact à la possibilité de soumettre des scripts coûteux, sans revendiquer de sortie du bac à sable ni d’exécution système. Le rapport privé date du 24 mars 2026. OpenCTI a intégré le premier correctif le 31 mars, publié la version 7.260401.0 le 1er avril et l’avis CVE-2026-35211 en juillet. Un durcissement complémentaire de la grammaire est arrivé en août ; il ne faut pas le confondre avec la première fermeture du chemin script.
Un filtre de requête avec un opérateur script
OpenCTI stocke ses objets de renseignement dans Elasticsearch ou OpenSearch et expose des listes via GraphQL. Ces requêtes acceptent des arbres de filtres : les opérateurs autorisés déterminent ce qu’un lecteur peut faire exécuter au moteur de recherche.
Le filtrage tient une place centrale dans cette API. La plupart des requêtes de liste, comme stixCoreObjects, acceptent un argument filters de type FilterGroup, un arbre d’objets Filter qui portent chacun une key, une liste de values et un operator. Le backend traduit cet arbre en requête Elasticsearch dans buildLocalMustFilter(). Comme la plateforme est faite pour être interrogée par de nombreux analystes et intégrations, le schéma GraphQL est de fait le contrat qui fixe ce qu’un utilisateur peut demander au cluster de recherche.
De l’énumération GraphQL à Painless
Le schéma GraphQL des versions vulnérables contient un membre inattendu dans l’enum FilterOperator, entre les opérateurs de chaîne et les tests de nullité :
enum FilterOperator {
eq not_eq lt lte gt gte match wildcard contains not_contains
ends_with not_ends_with starts_with not_starts_with
script
nil not_nil search within only_eq_to not_only_eq_to
}
Tous les autres opérateurs décrivent une comparaison. script décrit un mécanisme d’exécution, ce qui pose la question de ce que le constructeur de requêtes fait de sa valeur. Le suivre jusque dans buildLocalMustFilter() y répond.
Cause racine
Dans opencti-platform/opencti-graphql/src/database/engine.ts (7.260326.0, vers la ligne 2541), la branche de cet opérateur est la suivante :
} else if (operator === 'script') {
valuesFiltering.push({
script: {
script: values[i].toString(),
},
});
}
La valeur de l’utilisateur devient la source d’une requête script Elasticsearch, sous la forme courte où une chaîne désigne le code du script dans le langage par défaut, Painless. Rien ne contrôle la valeur, et rien ne réserve l’opérateur aux appels internes : la seule utilisation légitime dans le code était interne, dans report.js, où le backend compte les objets référencés par un seul rapport avec doc['…keyword'].length == 1.
Painless est le langage de script d’Elasticsearch (et d’OpenSearch). Dans une requête script, le script s’exécute en contexte de filtre : il est évalué pour chaque document candidat, lit les doc values de ce document via doc['champ'] et doit renvoyer un booléen qui décide si le document correspond. Painless s’exécute dans un bac à sable, avec une liste autorisée de classes et de méthodes et sans accès aux fichiers, aux processus ni au réseau : l’injection ne mène donc pas à une exécution de code sur l’hôte. Elle donne malgré tout à l’appelant un langage généraliste exécuté dans le cluster de recherche, avec boucles et calculs, évalué une fois par document. Deux faiblesses se cumulent : la génération de code à partir d’une entrée utilisateur (CWE-94), et un modèle d’autorisation qui laisse une capacité de lecture piloter l’exécution de scripts dans la couche de données (CWE-863).
Exploitation
Toute requête de liste qui accepte un FilterGroup peut porter l’opérateur. Avec le script inoffensif utilisé dans les tests unitaires d’OpenCTI, une requête prend cette forme :
POST /graphql HTTP/1.1
Authorization: Bearer <jeton d’un utilisateur doté de KNOWLEDGE>
Content-Type: application/json
{"query": "query { stixCoreObjects(first: 1, filters: {mode: and, filterGroups: [], filters: [{key: [\"name\"], operator: script, values: [\"doc.containsKey('name.keyword')\"]}]}) { edges { node { id } } } }"}
Le backend ajoute alors cette clause à la requête Elasticsearch, à côté des restrictions d’accès qu’il calcule pour l’utilisateur :
{ "script": { "script": "doc.containsKey('name.keyword')" } }
L’avis porte sur la disponibilité. Un script qui effectue un long calcul avant de répondre, par exemple des boucles imbriquées proches de la limite d’itérations que Painless impose à chaque exécution, consomme du CPU sur chaque document visité par la requête, et les index de connaissances d’une instance OpenCTI active contiennent de très nombreux documents. Répéter ces requêtes occupe les threads de recherche du cluster, dont dépendent tous les utilisateurs, connecteurs et tâches de fond de la plateforme.
Impact
L’avis GitHub note la faille 6,5 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H). PR:L correspond à la capacité KNOWLEDGE, la capacité de base pour accéder à la base de connaissances, que les rôles d’analyste incluent normalement. A:H traduit le fait que la cible est le cluster de recherche partagé : quand il sature, toute la plateforme ralentit ou cesse de répondre.
Le titre de l’avis mentionne aussi une exfiltration de données, mais son contenu et son vecteur ne retiennent que la disponibilité (C:N). Les éléments disponibles pour cet article ne montrent pas de chemin vers la confidentialité, et il n’en revendique donc aucun. Côté intégrité, une requête script ne peut pas modifier de documents ; elle décide seulement lesquels correspondent.
Correctif
OpenCTI 7.260401.0, publié le 01/04/2026, contient le commit b134cce (#15284), intitulé « move script usage to internal_script and remove by default from API ». L’opérateur script reste dans l’enum GraphQL, mais le constructeur de requêtes le refuse désormais sauf si un paramètre de configuration l’active :
// opencti-graphql/src/database/engine.ts (7.260401.0)
} else if (operator === 'script' || operator === 'internal_script') {
if (operator === 'script' && !isEsScriptFilterEnabled()) {
throw UnsupportedError('Filter script is not allowed', { filter: validFilter });
} else {
valuesFiltering.push({ script: { script: values[i].toString() } });
}
}
// opencti-graphql/src/database/engine-config.ts (7.260401.0)
const ES_SCRIPT_FILTER_ENABLED: boolean = booleanConf('elasticsearch:unsecure_script_filter_enabled', false);
L’usage interne de report.js passe à un nouvel opérateur internal_script, absent de l’enum GraphQL. La page des changements incompatibles de la documentation consigne la suppression et indique comment réactiver l’opérateur « at your own risk » avec ELASTICSEARCH__UNSECURE_SCRIPT_FILTER_ENABLED=true ; son exemple JSON utilise une clé nommée script_filter_enabled, alors que le code lit elasticsearch:unsecure_script_filter_enabled. Le même contrôle figure dans les versions 6.9.29 et 7.260309.0-lts.3, bien que l’avis ne mentionne que les versions antérieures à 7.260401.0.
Ce premier correctif laissait encore internal_script accepté par buildLocalMustFilter() pour tout appelant capable de transmettre un filtre sans passer par l’enum GraphQL. Un commit ultérieur, 1589eb3 (#17510, 06/08/2026, publié dans la 7.260807.0), retire internal_script de la grammaire des filtres, déplace la requête des rapports vers une option distincte internalScriptFilters alimentée uniquement par le code du backend, rejette tout opérateur inconnu et ajoute un test qui envoie un filtre JSON brut avec internal_script et attend un échec. Le même commit cesse aussi d’interpoler la clé de filtre dans le code Painless de l’opérateur only_eq_to, en la passant via params.field, et limite les clés de filtre à des segments [A-Za-z0-9_*-]. Ces changements ferment les chemins restants entre une entrée de filtre et le code d’un script.
Détection et mitigation
- Mettre à jour OpenCTI vers la 7.260401.0 ou une version ultérieure (ou vers la 6.9.29 ou la 7.260309.0-lts.3 sur ces branches), de préférence vers la 7.260807.0 ou au-delà pour le durcissement complémentaire, et vérifier que
ELASTICSEARCH__UNSECURE_SCRIPT_FILTER_ENABLEDn’est pas positionné àtrue. - Rechercher dans les journaux d’accès de l’API OpenCTI, ou au niveau d’un reverse proxy capable d’analyser GraphQL, les corps de requête contenant
operator: scriptou"operator":"script"; les clients légitimes de l’API publique n’ont aucune raison de l’envoyer. - Sur les versions corrigées, alerter sur l’erreur « Filter script is not allowed » dans les journaux du backend, qui identifie les comptes qui essaient encore.
- Activer le slow log Elasticsearch sur les index d’OpenCTI et rechercher les requêtes contenant une clause
scriptà la source inhabituelle, associées à une saturation durable du CPU des threads de recherche. - Passer en revue les rôles et comptes de connecteurs qui détiennent
KNOWLEDGE, et ne rendre le cluster de recherche joignable que depuis la plateforme OpenCTI et ses workers.
Chronologie
| Date | Événement |
|---|---|
| 24/03/2026 | Signalement privé aux mainteneurs via un avis de sécurité GitHub |
| 31/03/2026 | Correctif intégré (b134cce, #15284) |
| 01/04/2026 | Publication d’OpenCTI 7.260401.0 |
| 01/07/2026 | Publication de l’avis GHSA-qpp6-p693-rmm4 avec CVE-2026-35211 |
| 06/08/2026 | Intégration du durcissement complémentaire de la grammaire des filtres (1589eb3, #17510) |
| 07/08/2026 | Publication d’OpenCTI 7.260807.0 avec ce durcissement |
Références
- Avis de l’éditeur · CVE-2026-35211 github.com/OpenCTI-Platform/opencti/security/advisories/GHSA-qpp6-p693-rmm4
- Fiche NVD · CVE-2026-35211 nvd.nist.gov/vuln/detail/CVE-2026-35211
- Fiche CVE.org · CVE-2026-35211 www.cve.org/CVERecord?id=CVE-2026-35211
- MITRE CWE-94 cwe.mitre.org/data/definitions/94.html
- MITRE CWE-863 cwe.mitre.org/data/definitions/863.html
- Dépôt du projet github.com/OpenCTI-Platform/opencti
Citer cet avis
rootzero. « CVE-2026-35211 : injection de scripts Painless dans OpenCTI via l’opérateur de filtre GraphQL script ». Recherche de vulnérabilités, rootzero, 1 juillet 2026. https://rootzero.eu/research/cve/cve-2026-35211-opencti-painless-script-injection/
#Recherche de vulnérabilités
Faites auditer votre code avant qu’un autre ne le fasse.
Votre équipe doit décider quoi corriger avant une livraison ou évaluer une dépendance critique. Nous définissons une mission autour de cette décision, avec des preuves et des priorités de correction expliquées.