k) Sécurité des communications du browser

YoutubeImage

Single Origin Policy & CORS

La Single Origin Policy (SOP) sont des règles appliquées par le browser afin :

  • de restreindre les interactions entre un document ou script chargé par une origine avec une ressource d'une autre origine ;
  • d'isoler des documents ou scripts malicieux, afin de réduire le risque des attaques.

Deux URL ont une même origine si ces caractéristiques sont les mêmes :

  • protocole ;
  • port ;
  • host ; l'URL pointe vers le même appareil connecté à internet ou à un réseau local.

Il est possible de relaxer la sécurité via des Cross Origin Resource Sharing (CORS).
CORS est un mécanisme qui utilise des headers HTTP pour indiquer aux browsers qu'ils peuvent autoriser les accès à des ressources d'origines différentes.

Cela signifie qu'une application web qui utilise une API ne peut le faire que si les ressources demandées à l'API proviennent d'une même origine, à moins que la réponse de l'API inclut les bonnes CORS (via des header HTTP).

Si l'on autorise trop d'origines, voici un exemple classique d'attaque :

GatsbyImage

Imaginez qu'un site d'une banque ne soit pas sécurisé avec des techniques modernes. Ce site utiliserait une IHM (https://my-bank.com), qui, via un formulaire, permettrait de faire un versement, sous réserve d'envoyer un cookie qui contiendrait une variable de session (simple mécanisme de sécurité) lors d'une requête à l'API de la banque.

Maintenant, prenons le cas d'un utilisateur qui adore jouer sur le web, un gamer en puissance. Il joue à un jeu de pinguins, mais soudainement, il est redirigé vers un site malicieux (https://malicious.com), qui lui offre un nouveau jeu avec des dinosaures. Ce site malicieux, en arrière plan, pourrait faire une requête vers la même API utilisée par https://my-bank.com.
Comme c'est le même browser utilisé par notre gamer, tant pour faire ses virements, que pour jouer, toute requête faite vers https://api.my-bank.com enverra d'office les cookies existants et associés au domaine api.my-bank.com. Les résultats peuvent être dramatiques : l'attaquant à la capacité de faire des versements jusqu'à vider le compte de notre pauvre gamer.

Bien sûr, grâce à la SOP appliquée par nos browser, par défaut, dès que le site malicieux communique avec l'API de la banque, celui-ci bloque l'accès aux ressources de l'API car l'origine du site malicieux est différente de l'origine de l'API.

Les CORS permettent de relâcher la sécurité, afin notamment, dans le scénario évoqué, d'autoriser l'origine https://my-bank.com à accéder à l'origine https://api.my-bank.com. En effet, c'est ce que le site de la banque souhaite.
Par contre, si la banque possède des développeurs nuls au niveau sécurité et que ceux-ci autorisent toutes les origines à interroger l'API, là, nous pourrions arriver au hacking décrit ci-dessus.

Nous allons voir comment nous pouvons communiquer entre un frontend et une API fonctionnant sous deux origines différentes, à l'aide de deux techniques différentes.

Autorisation d'origines & les CORS

A cette partie-ci, nous allons voir comment mettre à jour une API afin que dans chaque réponse faite à un client, on ajoute un header permettant d'autoriser une ou plusieurs origines.

Pour ce nouveau tutoriel, nous allons partir de la dernière version de la RESTful API de pizzas.

Au sein de votre repo web2, veuillez créer le projet nommé /web2/tutorials/pizzeria/api/cors sur base d'un copié collé de /web2/tutorials/pizzeria/api/fat-model (ou fat-model).

Pour la suite du tutoriel, nous considérons que tous les chemins absolus démarrent du répertoire /web2/tutorials/pizzeria/api/cors.

Dans ce projet, veuillez installer le package cors :

bash
npm i cors

Nous allons configurer les headers de la RESTful API à l'aide du middleware cors offert par la librairie cors.

Pour configurer et utiliser les CORS, veuillez mettre à jour le fichier /app.js :

js
1
const express = require('express');
2
const cookieParser = require('cookie-parser');
3
const logger = require('morgan');
4
const cors = require('cors');
5
6
const corsOptions = {
7
origin: 'http://localhost:8080',
8
};
9
10
const usersRouter = require('./routes/users');
11
const pizzaRouter = require('./routes/pizzas');
12
13
const app = express();
14
15
app.use(logger('dev'));
16
app.use(express.json());
17
app.use(express.urlencoded({ extended: false }));
18
app.use(cookieParser());
19
20
app.use('/users', usersRouter);
21
app.use('/pizzas', cors(corsOptions), pizzaRouter);
22
23
module.exports = app;

Ici, nous précisons que l'API doit autoriser l'origine associée au port sur lequel tourne le serveur de fichiers statiques de Webpack (8080). Notons que nous n'avons pas relâché la sécurité pour les ressources de type "users", la fonction middleware cors n'est pas appelée au niveau du router de pizza.

Veuillez stopper l'éventuelle ancienne version de l'API, si elle est exécutée, et démarrer votre nouvelle version de l'API (/web2/tutorials/pizzeria/api/cors).

Il vous reste à vous assurer que votre frontend (/web2/tutorials/pizzeria/hmi/basic-fetch-no-proxy) est lui aussi bien démarré et peut réaliser son fetch implémenté dans HomePage.js.

Tout fonctionne correctement ?

Normalement oui, vous devriez avoir le site de la pizzeria qui affiche le menu des pizzas suite à un appel à l'API.

Si tout fonctionne bien, faites un commit de votre repo (web2) avec comme message : api-cors tutorial.

En cas de souci, vous pouvez accéder au code du tutoriel ici :

💭 OK, ça fonctionne bien... Mais cela est possible seulement si nous sommes les propriétaires de la RESTful API. Maintenant, que faire si une API tierce doit être intégrée dans notre frontend ?
Imaginez que vous souhaitez intégrer une opération d'une API offerte par Google...
Pensez-vous que vous pouvez leur donner un coup de fil et dire : "Google, peux-tu STP autoriser l'origine associée à mon site web ?"...
Nous allons voir un autre moyen de contourner la SOP (Single Origin Policy) imposée par le browser.

Simulation d'une même origine via un proxy

Il est possible de mettre en place un proxy au niveau du frontend afin de faire croire au browser que l'API et le frontend ont la même origine.

Ici, nous sommes dans la situation où nous ne souhaitons pas, ou nous n'avons pas les moyens, d'ajouter des origines au niveau de l'API.

Veuillez stopper l'exécution de l'API (/web2/tutorials/pizzeria/api/cors) et du frontend (/web2/tutorials/pizzeria/hmi/basic-fetch-no-proxy).

Veuillez démarrer l'API qui n'autorise aucune autre origine : /web2/tutorials/pizzeria/api/fat-model (ou via le code de ce web repo si vous avez un souci : api-fat-model).

Nous allons mettre en place un mécanisme au niveau du frontend pour faire passer toutes les requêtes à destination de l'API par un proxy ; le proxy aura la même origine que le serveur de fichiers statiques ayant offert le frontend.

Pour ce nouveau tutoriel, au sein de votre repo web2, veuillez créer le projet nommé /web2/tutorials/pizzeria/hmi/basic-fetch sur base d'un copié/collé de /web2/tutorials/pizzeria/hmi/basic-fetch-no-proxy (ou basic-fetch-no-proxy-hmi).

Il vous reste aussi à vous assurer que votre frontend (/web2/tutorials/pizzeria/hmi/basic-fetch) est lui aussi bien démarré. Il devrait toujours y avoir l'erreur associée aux CORS donnée au sein de la console.

Voici le workflow que nous allons appliquer à notre site gérant la pizzeria :

GatsbyImage
Redirections des requêtes via un proxy

Grâce à ce diagramme, on voit comment mettre à jour le tutoriel précédent qui affichait une erreur au niveau des CORS : on va faire une requête GET vers /api/pizzas et non plus vers http://localhost:3000/pizzas.

Le serveur de développement de Webpack met à disposition un proxy. Celui-ci est d'ailleurs configuré ainsi dans le boilerplate (voir fichier ./webpack.config.js) :

js
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: { '^/api': '' },
},
},

Cela signifie qu'à chaque fois qu'une requête sera faite sur /api (on reste sur la même origine que le serveur de fichiers statiques, 8080 tel que configuré dans ./webpack.config.js du boilerplate), celle-ci sera redirigée vers le port 3000, le port de l'API. Le pathRewrite permet de ne pas reprendre /api dans l'URL de la redirection : GET /api/pizzas devient GET http://localhost:3000/pizzas.

Dans le code du tutoriel en cours (/web2/tutorials/pizzeria/hmi/basic-fetch), veuillez mettre à jour l'URL au niveau du fetch dans HomePage.js :

js
1
const HomePage = () => {
2
clearPage();
3
4
fetch('/api/pizzas')
5
.then((response) => {
6
if (!response.ok) throw new Error(`fetch error : ${response.status} : ${response.statusText}`);
7
return response.json();
8
})
9
.then((pizzas) => {
10
renderMenuFromString(pizzas);
11
attachOnMouseEventsToGoGreen();
12
renderDrinksFromNodes(DRINKS);
13
})
14
.catch((err) => {
15
console.error('HomePage::error: ', err);
16
});
17
};

A ce stade-ci, tout devrait fonctionner : le menu des pizzas est affiché suite à l'appel à notre RESTful API ne relaxant pas la sécurité !

Si tout fonctionne bien, faites un commit de votre repo (web2) avec comme message : basic-fetch-hmi with proxy tutorial.

En cas de souci, vous pouvez utiliser le code du tutoriel :

🍬 Voici quelques infos non capitales pour ce cours-ci :

  • Il existe une multitude de proxy pour un environnement de développement : Webpack devServer et son proxy, VS Code proxy, proxy léger de Node directement configurable via package.json ("proxy": "http://localhost:3000",), ...
  • Il existe par exemple un proxy complet sous Node : http-proxy-middleware.
  • Pour la production, lorsque vous déployer une application web sur le cloud, il faudra trouver les instructions de votre provider pour voir comment configurer le proxy.
    Par exemple, pour configurer un static file server et son proxy sous heroku (provider de services d'hébergements sur le cloud), il faut configurer le fichier /static.json.

💭 Que pensez-vous du code associé aux tâches asynchrones, afin de chaîner des actions suite au fetch ?
Ca n'est pas des plus lisibles... imaginez que vous ayez des tonnes de .then() à gérer, dans lesquels vous allez aussi faire appel à des fonctions asynchrones... Ca deviendra vite compliqué comme code.

Nous allons donc tout prochainement voir comment rendre la programmation asynchrone plus légère, à l'aide de promesses et de async / await.

Projet 2.17 : consommation d'opérations d'une API à l'aide d'un proxy

Veuillez enrichir la SPA développée dans le cadre de votre projet en lui ajoutant :

  • Une RESTful API offrant une opération de lecture sur des ressources ;
  • Une page à votre frontend pour afficher les ressources offertes par l'opération de lecture de la RESTful API.

Dans un premier temps, identifiez le type de ressources qui aurait du sens à être affichée dans votre SPA. Cela sera peut-être des ressources de type "événements", "utilisateurs", "scores", "recettes"...

Vous allez donc développer deux applications :

  • Pour votre frontend : veuillez repartir du code de Projet 2.14.
    Le code du frontend doit se trouver dans votre repository local et votre web repository (normalement appelé web2) dans le répertoire nommé /project/2.17/frontend.
  • Pour votre API : le code doit se trouver dans votre repository local et votre web repository (normalement appelé web2) dans le répertoire nommé /project/2.17/api.

Au niveau du frontend, vous devez afficher des ressources offertes par une opération de lecture de votre API, en utilisant le proxy offert par webpack !

Quand un prototype de frontend est finalisé, veuillez faire un commit de votre code avec comme message : 2.17 : frontend basic proxy to render ressources.

Project 2.18 : Autorisation de nouvelles origines

Vous devez mettre à jour l'API développée pour Project 2.17 afin d'autoriser les origines locales fonctionnant sur ces ports : 8080, 7000 et 666.

Vous devez aussi mettre à jour le frontend développé pour Project 2.17 pour ne plus utiliser le proxy du frontend. Vous consommez directement l'API en indiquant son URL complète, y compris le port.
Par exemple, si vous traitiez de ressources de type "films", vous devez faire un fetch avec comme URL : http://localhost:3000/films.

Vous allez donc adapter deux applications :

  • Pour votre frontend : veuillez repartir du code de Project 2.17.
    Le code du frontend doit se trouver dans votre repository local et votre web repository (normalement appelé web2) dans le répertoire nommé /project/2.18/frontend.
  • Pour votre API : veuillez aussi repartir du code de Project 2.17. Le code doit se trouver dans votre repository local et votre web repository (normalement appelé web2) dans le répertoire nommé /project/2.18/api.

Quand un prototype d'api est finalisé, veuillez faire un commit de votre code avec comme message : 2.18 : api with new origins.

Quand un prototype de frontend est finalisé, veuillez faire un commit de votre code avec comme message : 2.18 : frontend render ressources with no proxy.