Intégrer la sécurité dans votre usine de développement JS

Spécialité(s)


Résumé

Développer une application qui répond aux besoins du client est compliqué. Développer une application répondant à l’ensemble des exigences non fonctionnelles est encore plus compliqué. Et développer une application industrialisée et sécurisée relève de l’exploit ! Mais, nous verrons dans cet article qu’à l’impossible nul n’est tenu…


Body

Nous verrons comment mettre en œuvre un pipeline de livraison continue mettant en œuvre des étapes qui nous permettront d’améliorer la sécurité. Puis, nous étudierons les résultats produits par notre pipeline. Enfin, nous nous intéresserons aussi aux limites et aux risques qu’impose l’usage d’un tel outil.

1. Un pipeline pour les gouverner tous

1.1 Un pipeline, mais pourquoi faire?

Cela sonnera comme un poncif : de plus en plus d’entreprises gèrent leurs projets suivant une méthodologie Agile afin de pouvoir livrer plus souvent de la valeur au client. Idéalement, dès que la fonctionnalité est implémentée pour livrer de la valeur au plus tôt.

Pour y parvenir, des méthodologies comme XP (Extreme Programming) mettent l’accent sur l’intégration continue. Le pipeline d’intégration continue (Continuous Integration) est une pratique où les développeurs intègrent, c’est-à-dire regroupent, leur code dans un espace de travail plusieurs fois par jour. À chaque nouvel ajout de code, une construction est déclenchée automatiquement dans le but de détecter le plus tôt possible les problèmes. L’objectif est d’avoir un retour rapide pour pouvoir corriger le plus tôt possible sur la qualité du code produit. Ainsi, les bogues sont détectés et sont bloqués sans aller en production. Nous voyons donc tout l’intérêt d’être capable d’injecter de la sécurité dans ce pipeline : dès qu’une nouvelle ligne de code est intégrée dans le logiciel, le développeur a rapidement un retour lui indiquant qu’une faille doit être corrigée. Le pipeline empêche ainsi que des anomalies se retrouvent en production !

On distingue en général trois approches pour son pipeline de livraison : intégration continue, livraison continue et déploiement continu. Pour un pipeline de livraison continue (Continuous Delivery), nous nous assurerons que tout changement peut être déployé en production. Cela ne veut pas dire qu’il le sera, mais il pourrait l’être. Cela oblige les équipes à faire en sorte que le logiciel soit fonctionnel à n’importe quel moment. Enfin, un pipeline de déploiement continu (Continuous Deployment) se différencie des autres types de pipelines par le fait que chaque changement est déployé en production. Cela augmente d’autant plus les exigences en termes de qualité et accroît aussi le risque au niveau de la sécurité. Il devient impossible d’attendre un éventuel audit sous peine de bloquer tout le processus de livraison.

article-4-cycle-integration

Figure 1 - Source originelle : https://www.collab.net/solutions/development-process.

Une bonne pratique est a minima de mettre en œuvre un pipeline de livraison continue. Cela oblige à exécuter tous les tests, qu’ils soient fonctionnels, de performance, de qualité et de sécurité.

1.2 Injecter de la sécurité dans mon pipeline/SecDevOps

Différents outils existent pour évaluer la sécurité d’un système. Nous les classerons en deux grandes catégories : l’analyse statique et l’analyse dynamique de code. Un outil d’analyse statique s’intéresse à étudier le logiciel sans l’exécuter tandis qu’un outil d’analyse dynamique teste l’application dans son contexte. Chacun de ces outils présente des avantages et des inconvénients. Le tableau ci-dessous détaille ces points :

 

Avantages

Inconvénients

Analyse statique

Analyse complète du code

Précis

Rapide

Remonte de nombreux faux positifs

Se limite généralement à des problèmes unitaires

Analyse dynamique

Teste l’outil dans sa globalité

Les cas de tests ne sont pas forcément exhaustifs

Certains outils intégrés dans le pipeline ne sont pas directement liés à la sécurité, mais aident à améliorer la qualité. Nous pouvons penser aux linters. Un linter est un outil qui vérifie le respect des normes dans le code. Le code est ainsi plus homogène. Cela facilite sa lecture et donc aide l’audit du code.

Une des premières étapes est d’évaluer les attaques possibles et de voir comment y répondre. Nous pourrions nous appuyer sur le top 10 de l’OWASP ou sur le TOP 25 SANS pour avoir une liste non exhaustive des risques probables à considérer. Cela nous aidera à concevoir notre pipeline.

La liste suivante présente les outils qui seront intégrés dans le pipeline. Ils ont été choisis pour plusieurs raisons. Ils sont open source, reconnus pour leur intérêt par rapport à notre besoin de sécurité et leur usage dans l’industrie. Ils sont surtout utiles et utilisables pour notre application web écrite en JavaScript.

Outil

Type

Failles couvertes

Description

Intérêt

eslint-security

Analyse statique

Security Misconfiguration

Bonnes pratiques en termes de sécurité pour JS

++

Peu coûteux, beaucoup de faux-positifs [TIM]

OWASP Zap

Analyse dynamique

Injection, XSS…

Tester la résistance de l’application face à certaines attaques

+

Nécessite de la configuration

Le crawler peut se « perdre » dans les pages

Mozilla Observatory

Analyse dynamique

XSS…

S’assurer que les best practices HTTP sont respectées

++

Simple et efficace

eslint

Autre (linter)

Code complexe

Vérifier que le code est bien formaté (facilite l’audit boîte blanche)

++

Utile pour la sécurité et pour les développeurs

Jest

Autre (runner de test)

Exécuter les tests unitaires

Un code de qualité est un code bien testé.

Exécute les tests unitaires liés à la sécurité

+++

À combiner avec une méthodologie de développement orientée test et sécurité

Stryker

Autre (mutation testing)

Fuzzing

S’assurer que les cas aux limites ont bien été testés

+

Potentiellement long à configurer, mais puissant

npm audit

Analyse statique

Dépendances vulnérables

Liste les packages du projet présentant une vulnérabilité connue

+++

Intégré par défaut dans npm

NoSqlMap

Analyse dynamique

NoSQL Injection

L’équivalent de SqlMap pour le NoSQL

++

Indispensable pour couvrir le risque n°1 du TOP 10 OWASP

+++ : indispensable, ++ : à considérer, + : optionnel

Une fois que nous avons sélectionné les différents outils que nous voulons utiliser, il reste à les mettre en œuvre de manière efficace. Nous allons voir comment construire un pipeline pour notre intégration continue sécurisée.

1.3 Un peu de Docker, monsieur Frodon?

Pour notre article, nous avons choisi de nous appuyer sur Jenkins en tant que serveur d’intégration continue. Il s’agit d’un outil open source très prisé en entreprise. De nombreux plugins sont présents pour les différentes technologies. De plus, il s’interface facilement avec Sonar.

Sonar est un serveur de suivi de qualité de code. Il permet de suivre et de visualiser l’évolution de la qualité de code au cours du temps. En un clin d’œil, nous avons une vue agrégée de l’état du code.

article-4-sonar

Figure 2

Un code de qualité est un élément essentiel dans la sécurité du code. Un code complexe est généralement peu compréhensible et résistera à une relecture dans le cadre d’une analyse boîte blanche. De plus, un code de mauvaise qualité contient souvent du code dupliqué et donc des failles à plusieurs endroits. Sonar contient aussi des règles de détection des vulnérabilités dans de nombreux langages, dont JavaScript. Il détectera ainsi l’usage d’eval qui permet d’exécuter de manière dynamique du code JavaScript. Quoi de mieux pour faire une injection ? On ne peut pas avoir de sécurité sans qualité. Sonar devient ainsi un composant de votre pipeline de sécurité. Petit bonus : Sonar embarque quelques règles. Dans la version utilisée (6.7.4), nous retrouvons 9 règles liées à la sécurité. C’est un bon début. Nous pouvons les compléter avec les autres outils mentionnés précédemment, comme eslint-security.

article-4-sonar-rules

Figure 3

Certaines équipes ont l’habitude de l’habitude de séparer le plus possible leur développement lorsqu’elles travaillent sur une même portion de code. Les interactions dues au travail en cours sont limitées. Elles gèrent ensuite les conflits uniquement au moment où elles terminent le développement d’une fonctionnalité. Pour effectuer cette séparation, il arrive qu’une branche soit créée lors de l’implémentation d’une nouvelle fonctionnalité (par exemple, en mode Feature Branching [BRANCH]). Les différentes phases d’un projet sont elles aussi séparées : développement, test, pré-production, production… Les équipes se retrouvent ainsi à devoir régulièrement modifier la configuration de Jenkins pour prendre en compte les ajouts ou suppressions de branches.

Depuis 2006, Jenkins simplifie la création de pipeline de développement. Ce dernier est décrit grâce à un fichier appelé Jenkinsfile [JENKINS]. Jenkins liste les branches et crée automatiquement un nouveau pipeline pour chaque branche possédant ce fichier. Cela permet d’exécuter l’ensemble de tests pour chaque branche. Nous détectons une faille dès qu’elle est présente.

Exemple d’étapes dans mon build :

stage('Test'){
   steps {
      sh 'npm run test'
   }
}
stage('Lint') {
   steps {

      sh 'npm run lint'
   }

}

Pour commencer, nous allons déployer deux serveurs :

  • Jenkins : en charge du pipeline ;
  • Sonar : en charge de la qualité de code.

Pour faciliter la mise en œuvre du projet en local, nous allons créer et configurer nos instances via Docker. Docker permet de lancer facilement des instances. Chaque instance est isolée dans un container dédié. Nous pouvons ainsi démarrer une instance Jenkins isolée. C’est généralement une problématique que nous retrouvons en entreprise. En effet, chaque équipe veut généralement son instance taillée selon les besoins de l’application qu’elle développe. En une commande, il est facile de configurer, instancier et démarrer tous les serveurs grâce à un fichier Docker-compose.yml qui contient la description de notre environnement. Pour démarrer notre plateforme :

sudo docker-compose up

Une fois démarré, il reste à créer un job pipeline à partir du fichier de configuration Jenkinsfile :

article-4-jenkins-pipeline

Figure 4 : Un pipeline comme on les aime.

Il ne reste plus qu’à activer notre pipeline. Voyons si les résultats sont au rendez-vous.

2. Résultats

Activons notre pipeline sur notre projet et sur l’application « Goof » de l’entreprise Snyk, une application vulnérable pour s’entraîner à la recherche de vulnérabilités, et observons les résultats obtenus.

2.1 Contribution de Jest et Stryker

Pourquoi parler d’un framework de test en sécurité ? Après tout, « tester, c’est douter ». Alors, doutons mon ami ! Tout d’abord, nous pouvons définir des tests pour vérifier la bonne implémentation des fonctionnalités de sécurité. Si vous avez déjà utilisé des frameworks comme Spring ou Express.js, vous savez que leur utilisation peut se révéler ardue. Il est souvent utile de définir des tests pour valider notre compréhension de ces frameworks, mais aussi pour vérifier que les contrôles de sécurité sont bien opérationnels.

L’exemple suivant permet de tester la bonne utilisation d’Express.js. Le premier test valide qu’un utilisateur non authentifié ne peut pas accéder à une page. Le deuxième test vérifie qu’un utilisateur authentifié peut y accéder.

describe('Application with express.js', () => {

   it('should redirect unauthenticated user to sign-page', done => {

      passport.use(new RejectStrategy());

      const app = buildApp('reject', passport);

      request(app)

         .get('/users/')

         .expect(302)

         .then(response => {

            expect(response.res.headers.location).toBe('/signin');

            done();

          });

   });

   it('should redirect unauthenticated user to sign-page', () => {

      const STRATEGY = 'mock';

      const user = {

         id: 1,

         name: 'USER',

         provider: STRATEGY,

      };

 

      passport.use(new mockStrategy({

         name: STRATEGY,

         user,

      }));

      const app = buildApp(STRATEGY, passport);

      request(app)

      .get('/users/')

      .expect(200);

   });

})

De manière plus générale, les tests ont de nombreux bénéfices. Couplés avec la méthodologie TDD (Test-Driven Development), ils guident le développeur vers une conception logicielle plus simple. Un code plus simple est plus facile à analyser. Nous y serons plus à même d’identifier les problèmes de sécurité dans un code « spaghetti ». Moins de code inutile veut aussi dire moins de chance d’introduire des anomalies. Pour lancer les tests via jest, rien de plus simple :

$ jest

Enfin, les tests servent aussi des tests de non-régression, une fois ajoutés à la base de code principale. Ils servent de filets de sécurité et génèrent des alarmes en cas d’introduction d’anomalies. En cas de détection d’une faille, nous devons être en mesure de livrer de manière fiable et rapide le correctif. Cependant, pour y arriver, il est nécessaire de vérifier que le correctif n’a pas cassé de fonctionnalités. Vos utilisateurs seront mécontents d’avoir un logiciel sûr, mais inutilisable. Encore faut-il que les tests soient bien écrits !

Pour écrire des bons tests, un conseil : tester les valeurs aux limites. Si, par exemple, je dois valider qu’une valeur entière est supérieure strictement à 0, il est nécessaire d’avoir deux tests : un pour la valeur 0 et un pour la valeur 1. Nous pouvons aller plus loin et introduire un outil qui va « tester vos tests ». Le « mutation testing » est une technique qui consiste à modifier le code de production et à lancer les tests. Il s’agit d’une technique très efficace. Si au moins un test est en erreur, le code est correctement testé. Nous ne pouvons pas modifier le code par erreur. Sinon, si aucun test n’échoue, votre code est mal testé. Si votre correctif modifie une partie de code mal testé, des bogues vous guettent ! La librairie [STRYKER] implémente cette technique côté JS.

Pour l’installer et exécuter Stryker dans votre projet, quelques lignes suffisent :

$ npm install --save-dev stryker stryker-api
$ node_modules/.bin/stryker init

$ node_modules/.bin/stryker run

Il est possible d’appeler Stryker à chaque commit. Le temps d’exécution peut être très long. Sur un « gros » projet, il est préférable de lancer la vérification de façon moins régulière, une fois par jour par exemple. Voici un exemple de sortie avec Stryker avec la liste des mutations sans effet :

15:19:16 (20754) INFO InputFileResolver Found 7 of 28 file(s) to be mutated.

15:19:18 (20754) INFO Stryker 96 Mutant(s) generated

Mutant survived!

/home/dev/project/routes/signin.js:9:8

Mutator: IfStatement

- if (req.isAuthenticated()) {

+ if (true) {

...

----------------|---------|----------|-----------|------------|----------|---------|

File | % score | # killed | # timeout | # survived | # no cov | # error |

----------------|---------|----------|-----------|------------|----------|---------|

All files | 22.92 | 22 | 0 | 74 | 0 | 0 |

routes | 27.66 | 13 | 0 | 34 | 0 | 0 |

index.js | 12.50 | 1 | 0 | 7 | 0 | 0 |

private.js | 0.00 | 0 | 0 | 7 | 0 | 0 |

signin.js | 32.00 | 8 | 0 | 17 | 0 | 0 |

users.js | 57.14 | 4 | 0 | 3 | 0 | 0 |

app.js | 31.03 | 9 | 0 | 20 | 0 | 0 |

index.js | 0.00 | 0 | 0 | 3 | 0 | 0 |

models/user.js | 0.00 | 0 | 0 | 17 | 0 | 0 |

----------------|---------|----------|-----------|------------|----------|---------|

15:19:39 (20754) INFO Stryker Done in 22 seconds.

2.2 Contribution de eslint-security

Nous obtenons l’erreur d’eslint :

> eslint src/ routes/ models/ *.js

/var/jenkins_home/workspace/Prototype_PassportJS/routes/signin.js

   4:32 warning Unsafe Regular Expression security/detect-unsafe-regex

En regardant la ligne en question, nous trouvons le code responsable de l’alerte :

const VALID_NAME_PATTERN = /^([a-z]+)+$/;

C’est une faille classique due à une mauvaise utilisation des expressions régulières : ReDos. Ici, une entrée spécialement conçue pourrait générer un traitement excessif. L’alarme nous permet de corriger rapidement notre code :

const VALID_NAME_PATTERN = /^([a-z])+$/;

2.3 Contribution de npm audit

Npm Audit produit la sortie suivante :

┌───────────────┬──────────────────────────────────────────────────────────────┐

│Low │ Regular Expression Denial of Service │

├───────────────┼──────────────────────────────────────────────────────────────┤

│Package │ uglify-js │

├───────────────┼──────────────────────────────────────────────────────────────┤

│Patched in │ >=2.6.0 │

├───────────────┼──────────────────────────────────────────────────────────────┤

│Dependency of │ jade │

├───────────────┼──────────────────────────────────────────────────────────────┤

│Path │ jade > transformers > uglify-js │

├───────────────┼──────────────────────────────────────────────────────────────┤

│More info │ https://nodesecurity.io/advisories/48 │

└───────────────┴──────────────────────────────────────────────────────────────┘
....found 2 low severity vulnerabilities in 23197 scanned packages
 2 vulnerabilities require manual review. See the full report for details.

Plusieurs dépendances présentant des vulnérabilités ont été trouvées. Un atout indispensable qui aide le développeur JavaScript. NPM propose plus de 700000 packages. Entre les packages vulnérables, que nous pouvons inclure directement ou ceux inclus par les dépendances, et les packages spécialement conçus pour être vulnérables, npm audit s’avère être un outil indispensable vu son apport.

2.4 Contribution d’OWASP Zap

Dans notre pipeline, nous utilisons le client zap en mode « quick-scan ». De nombreux scanners de vulnérabilités sont présents. Vous pourrez lister zap-cli scanners list. Par défaut, seuls les scanners XSS sont activés, mais vous pouvez activer d’autres scanners pour trouver des failles de type « Path Traversal » par exemple.

Lors de l’exécution, OWASP Zap remonte l’erreur suivante :

[INFO] Running a quick scan for http://localhost:9080

[INFO] Issues found: 1
| Alert | Risk | CWE ID | URL |
| Cross Site Scripting (Reflected) | High | 79 | http://localhost:9080/?q=%3C%2Fp%3E%3Cscript%3Ealert%281%29%3B%3C%2Fscript%3E%3Cp%3E |

En regardant le code, nous nous rendons compte que nous avons utilisé la mauvaise balise pour Jade. Au lieu de s’appuyer sur l’élément #{} qui échappe les balises comme <script>!{} a été utilisé. Résultat, une belle faille XSS ! Corrigeons-la de suite.

2.5 Contribution de Mozilla Observatory

Il n’existe pas de plugins pour Mozilla Observatory dans Jenkins. Qu’à cela ne tienne, il est facile de l’insérer dans notre pipeline. Pour ce faire, nous capturons l’entrée et extrayons la note rendue par l’outil.

Score: 0 [F]

Modifiers:

Content Security Policy [-25] Content Security Policy (CSP) header not implemented

Contribute              [  0] Contribute.json isn't required on websites that don't belong to Mozilla

Cookies                 [  0] No cookies detected

...

Le précédent article présente un exemple de résultats que nous avons pu obtenir. Nous corrigeons rapidement les erreurs trouvées pour obtenir une meilleure note.

2.6 Contribution de NoSqlMap

Actuellement, NoSqlMap fonctionne uniquement en mode interactif. Un auditeur doit donc être présent pour définir les actions. Nous ne pouvons pas automatiser les actions. Bon, pas très pratique pour un pipeline censé être automatique! Qu’à cela ne tienne, comme c’est un projet open source, nous pouvons le modifier. Nous avons donc ajouté une interface pour le composant de scan de la partie web et nous l’avons ajouté à notre pipeline de livraison continue [PR].

Il nous suffit alors d’appeler NoSqlMap sur les pages que nous voulons tester puis de traiter le résultat en sortie.

python2 nosqlmap.py --attack=2 --victim=localhost --webPort=3000 --uri="/?q=" \
 --injectSize=1000 --injectFormat=1 --params=1 \
 --doTimeAttack=n --injectedParameter=2

De même, quelques alertes et suspicions de failles sont détectées. Un travail d’analyse est nécessaire pour séparer le « vrai du faux » problème.

3. On ne crée pas simplement un pipeline dans le cloud

Un pipeline sécurisé, c’est magique ! Oui, mais ce n’est pas gratuit… ni la panacée ! Notre pipeline nous permet de détecter des failles de type XSS, les paquets vulnérables, les erreurs de configuration ou encore les injections.

Il faut prendre en compte plusieurs points :

  • Un outil ne capturera jamais des failles conceptuelles comme des fonctionnalités présentant des failles évidentes. Un métier pourrait très bien demander : « Est-ce que vous pourriez me créer une page avec une URL cachée ? Personne ne la trouvera. On fait ça pour ne pas s’embêter avec la sécurité et aller plus vite ».
  • Les outils ont tendance à remonter beaucoup de faux positifs ou d’alarmes déjà prises en compte. À la fin, les équipes de développement pourraient finir par ignorer complètement les alarmes.
  • Notre pipeline devient lui-même une cible privilégiée pour un attaquant, notamment dans le cas du continuous deployment. Pour pouvoir déployer en production, celui-ci doit avoir les identifiants (comme des clés SSH par exemple) pour installer l'application. Quoi de mieux pour un attaquant que d’avoir directement accès aux serveurs de production ? Et dans les autres cas, l’application générée via le pipeline peut être modifiée par un attaquant au moment de la compilation du code. Et même si le serveur est dans une zone protégée, le code présent sur le serveur de gestion de configuration devient lui-même une cible de choix, puisque les barrières sont levées. Bref, toute une chaîne de production à protéger en permanence.
  • Les efforts faits pour notre application doivent aussi être faits pour le pipeline. Plusieurs questions se posent : le plugin que je veux installer est-il sûr ? Est-ce que le serveur d’intégration continue est-il à jour ? Correctement configuré ? Il est nécessaire d’inclure le pipeline dans notre analyse de risque et de le traiter avec autant de précautions que notre application.

Bref, comme toujours, il est nécessaire de ne pas oublier le moindre maillon dans la chaîne sous peine de donner des sueurs froides à votre auditeur sécurité lors de son prochain passage.

Conclusion

Pour livrer toujours plus vite de la valeur à nos utilisateurs, il est indispensable d’automatiser toujours plus notre travail afin de fiabiliser les processus de production et libérer des ressources. Un cercle vertueux en somme. Et la sécurité n’est pas en reste ! Cependant, il n’est pas possible de toujours tout automatiser et notamment d’automatiser à un prix raisonnable. Un risque qu’il faut mesurer au vu des gains et des coûts que cela peut engendrer. La sécurité dans le pipeline de production moderne s’avère nécessaire, mais pas suffisante. Il serait trompeur de penser que cela remplace des équipes sensibilisées à la sécurité. De plus, d’autres pratiques, comme le Security Chaos Engineering, peuvent venir en complément dans une vision d’automatisation de la sécurité. Avec le temps, votre pipeline sera rapidement votre « précieux », qui vous secondera dans la sécurité !

Remerciements

Un grand merci à Olfa Mabrouki, Olivier Poncet, Jordan Noury et Julien Topçu pour leur relecture et Didier Bernaudeau pour cette collaboration !

Ressources

[PR] NoSQLMap – Pull Request 69 : https://github.com/codingo/NoSQLMap/pull/69

[BRANCH] Feature Branch : https://martinfowler.com/bliki/FeatureBranch.html

[TIM] detect-possible-timing-attacks : https://github.com/nodesecurity/eslint-plugin-security

[JENKINS] Jenkins Pipeline : https://jenkins.io/doc/book/pipeline/

[STRYKER] Stryker : https://stryker-mutator.io/



Article rédigé par

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous