Skip to content

Bonnes pratiques

Sécurité

Ce document décrit une série de bonnes pratiques à mettre en œuvre pour accroître la sécurité et les performances d'une application Ewt.

  • Contextualiser les scripts
  • Ajouter des blocs "policy" lorsque cela est nécessaire
  • Éviter de laisser des servlets non protégés (au niveau du web.xml, si besoin mettre une exception sur les URL rest qui sont autorisées sans authentification)
  • utiliser des annotations @accept ou @reject sur les scripts
  • appliquer les règles de sécurité au niveau du serveur d'application (web.xml et autre)

Autres recommandations à mettre en œuvre

  • Éviter d'utiliser disable-output-escaping="yes" et xsl:copy-of au niveau des feuilles de styles. S'il n'est pas possible d'y échapper, s'assurer que la valeur source a été neutralisée au moyen de sanitize.
  • Toutes les réponses doivent contenir X-Content-Type-Options : nosniff
  • Toutes les réponses doivent être en HTTP Strict Transport Security headers (HSTS), y compris les sous-domaines
  • Referrer-Policy doit être utilisé avec la valeur no-referrer, same-origin ou strict-origin-when-cross-origin
  • X-Frame-Options doit être utilisé avec le paramètre adéquat suivant l’application (en principe sameorigin)
  • Access-Control-Allow-Origin ne doit pas être utilisé avec * (toujours préciser l’origine)
  • Le serveur web ne doit accepter que les méthodes HTTP utilisées par l’application ou l’API
  • Origin header ne doit pas être utilisé comme moyen d’authentification
  • Cross-Origin Resource Sharing (CORS) doit être utilisé uniquement avec une liste blanche de domaines de confiance, et la valeur null comme origine est à exclure
  • Les messages d’erreurs ne doivent pas livrer d'information sur la configuration du système, sur les versions et les connexions
  • Le mode debug et http Trace doivent être désactivés en production
  • Les requêtes et réponses ne doivent pas exposer d'informations sur la configuration du système, sur les versions et les connexions

Sécurité

Bloc security du config.xml

Par défaut, une application active une série d'options de sécurité. Le bloc security du config.xml permet de désactiver certaines de ces options, ce qui peut être utile pour des tests, mais ne devrait pas être fait sur les environnements de production.

La documentation relative au bloc security décrit le rôle des différents éléments.

CSRF (Cross-Site Request Forgery)

CSRF est un type d'attaque permettant de lancer des commandes au serveur en utilisant la session d'un autre utilisateur. Le moteur implémente une solution de protection contre CSRF. Dans certains cas, la protection CSRF nécessite que le client (c.-à-d. le navigateur) adapte son code pour être compatible. Avant cela, voici un petit résumé de la technique d'attaque CSRF via un exemple.

Principe de l'attaque

Pour cet exemple, nous allons simplement tenter d'appeler un script par l'intermédiaire d'un formulaire HTML simple. Le code du script et du formulaire de test est donné ci-après:

1
$logger.error("=============== 😈 YOU'VE BEEN HACKED ! 😈 ===============");
<html>
    <body>
        <form method="post" action="https://server-host:8080/ewt/web/unittest">
            Session:
            <input type="text" name="EWT:SESSIONID"
                   value="11111111-1989-48ef-9835-baa8101a087a"/>
            <br>
            Command:
            <input type="text" name="EWT:COMMAND"
                   value="{&quot;action&quot;:&quot;script&quot;,&quot;params&quot;:{&quot;name&quot;:&quot;hacktest&quot;}}"/>
            <br>
            <input type="submit" value="Test !"/>
        </form>
    </body>
</html>

Le principe de l'attaque est de mettre en ligne le formulaire html ci-dessus sur un serveur tiers (autre que celui de l'application Ewt, par exemple sur un domaine "hacking-host.com") et de pousser un utilisateur légitime connecté à une application Ewt à débrancher sur ce formulaire et faire en sorte qu'il l'envoie depuis son navigateur.

Le navigateur se chargera alors d'envoyer le formulaire sur l'adresse "https://server-host:8080/ewt/web/unittest" indiquée dans l'attribut "action" du formulaire. Ce faisant, le navigateur y joindra le jeton de session de l'utilisateur. La requête étant parfaitement valide du point de vue du serveur et l'utilisateur disposant des droits suffisants, la commande est passée.

Petite remarque en passant: dans l'illustration ci-dessus, les champs de formulaire ainsi que le bouton "submit" sont visibles. Une personne mal intentionnée prendra évidemment le soin de masquer les champs et de remplacer le bouton d'envoi par une commande javascript qui peut s'exécuter sans intervention de l'utilisateur.

Principe de la protection Ewt

Plusieurs mécanismes sont mis en oeuvre du côté Ewt. Le moteur s'appuie à la fois sur l'identifiant de session et un token véhiculé au moyen d'un cookie avec option "SameSite" en mode "Strict".

Nom du cookie

Le nom du cookie utilisé pour véhiculer le token CSRF est différent pour chaque session Ewt. Il est construit avec un préfixe commun ewt-csrf-token suivi d'une valeur uuid qui est spécifique à la session Ewt (mais qui n'est pas l'id de session lui-même).

L'usage d'un nom de cookie spécifique à chaque sessoin Ewt prévient des conflits possibles lorsque l'utilisateur ouvre plusieurs sessions Ewt depuis différents onglets d'un même navigateur.

Contrôle de l'identifiant de session

Lorsqu'une requête arrive, Ewt vérifie en premier lieu que l'identifiant de session qu'il reçoit est valide du point de vue de sa forme. Il recherche ensuite s'il existe une session correspondant à cet identifiant sur le serveur, que ce soit en mémoire ou en base de données (ce fallback est nécessaire pour supporter le failover).

Si aucune session correspondante n'est trouvée et que la méthode HTTP est autre que GET, alors le traitement est abandonné.

Contrôle du token CSRF

Le contrôle du token s'effectue dans les cas suivants:

GET POST
web pas de check check
rest check check

Prenons le cas du servlet web: lorsque la session a pu être identifiée et que la requête est un POST, le moteur recherche alors le cookie dans lequel le token CSRF est censé être véhiculé. Il analyse alors ce dernier et vérifie qu'il est valide en régénérant la valeur HMAC à partir de l'identifiant de la session, d'une clé secrète interne propre à chaque session et d'un sel contenu dans le cookie. En cas de différence entre le hash attendu et le hash reçu, la requête est rejetée.

Conséquence pour une application

La mécanique nécessite que l'identifiant de session soit toujours véhiculé lorsque le contrôle CSRF s'applique. Cela impose donc l'envoi de deux éléments dans les requêtes envoyées à Ewt:

  • l'identifiant de session
  • le cookie véhiculant le token CSRF

Dans le cas des formulaires HTML, l'identifiant de session peut être transmis au moyen du champ EWT:SESSIONID. Le cookie est quant à lui automatiquement transmis par le navigateur.

Dans le cas de requêtes XmlHttpRequest, l'identifiant de session peut être transmis via le header x-ewt-sessionid. Le cookie quant à lui est également automatiquement transmis par le navigateur.

Requêtes SQL

👍 Utiliser les prepared statements

Le moteur supporte l'usage des prepared statements et il est fortement recommandé de s'appuyer sur la façon qu'elle permet pour le passage de paramètre.

Performances

Références de variables et de champs

👍 Éviter les références du genre ${variable} lorsque cela est possible et privilégier la référence directe variable

👍 Éviter les références du genre ${data:field} lorsque cela est possible et privilégier la notation sharp #field

Le traitement de références du genre ${variable} au sein d'une chaîne de caractères est relativement coûteux en termes de traitement car cela oblige le processeur de script à effectuer un parsing de la chaîne afin d'identifier les références, puis à les résoudre.

Dans la mesure du possible, il est préférable de référencer les variables directement: dans ce cas, le traitement peut être pré-effectué lors du parsing du script. Ce travail n'est donc plus à faire lors de l'exécution.

L'exemple ci-dessous donne deux variantes d'un code qui, in fine, produisent le même résultat. On effectue deux boucles de 1 mio d'itérations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var txt, tbeg;
tbeg = $cal.timestamp();
for (var i = 0; i < 1000000; ++i) {
    var v = [ i ];
    txt = "${v[0]}${v[0]}${v[0]}${v[0]}${v[0]}${v[0]}${v[0]}${v[0]}${v[0]}${v[0]}";
}
$logger.info("Test 1: " & $cal.diff(tbeg, $cal.timestamp()));

tbeg = $cal.timestamp();
for (var i = 0; i < 1000000; ++i) {
    var v = [ i ];
    txt = v[0] & v[0] & v[0] & v[0] & v[0] & v[0] & v[0] & v[0] & v[0] & v[0];
}
$logger.info("Test 2: " & $cal.diff(tbeg, $cal.timestamp()));

Dans le premier cas, on construit une chaîne de caractères en concaténant plusieurs fois la valeur v[0] au moyen de la notation ${v[0]}.

Dans le second cas, on concatène également la valeur v[0], mais cette fois en utilisant l'opérateur &.

La seconde boucle est 3 fois plus rapide que la première. La première boucle nécessite 15.44 secondes de traitement alors que la seconde s'exécute en 4.8 secondes seulement.

Il en va de même pour les références de champs. La notation ${data:field} permet de référencer la valeur d'un champ. Elle est pratique pour intégrer une valeur de champ au sein d'une chaîne, mais il est préférable de passer par la notation sharp #field lorsque cela est possible.

Passage de paramètre par référence

👍 Utiliser le passage de paramètre par référence plutôt que par valeur dans le cas de gros objets

Lorsqu'on effectue un appel de fonction du genre traiter(objet), le moteur passe à la fonction traiter une copie de objet. La copie nécessite d'allouer de la mémoire sur le serveur et d'effectuer des transferts de données pour copier l'objet. Tout cela nécessite du traitement.

Il est possible de passer un objet par référence, c'est-à-dire que dans ce cas l'objet source n'est plus copié, mais référencé. C'est l'objet lui-même qui est passé à la fonction. Le traitement au niveau du serveur est plus simple et plus rapide.

Pour indiquer que l'on souhaite passer un objet par référence, il suffit
de préfixer le nom de l'objet avec un &. Cela donne: traiter(&objet).

Requêtes SQL

👍 Utiliser des batchs pour les traitements par lots

Le moteur supporte l'utilisation des prepared statements. Cela offre une sécurité accrue et empêche l'injection SQL. Les prepared statements permettent également la réutilisation de requête pré-parsée et l'exécution batchs.

Ewt propose deux syntaxes pour utiliser le principe des prepared statements: le passage de paramètres en table ou en map.

Syntaxe utilisant une table de paramètres

Cette syntaxe est la plus standard et la plus directe car elle est nativement supportée par les drivers JDBC. Le principe est de représenter les valeurs à insérer dans la requête au moyen du caractère ?, puis de passer un tableau de valeur en paramètre à la requête.

Voici un exemple de requête utilisant un prepared statement:

1
2
3
4
$sql.update(`update Vendeur
             set Nom=?, Prenom=?
             where idVendeur=?`::T,
            [ "O'Reilly", "John", 14 ]); 

La valeur "O'Reilly" viendra remplacer le premier ? (notez au passage qu'il n'y a pas besoin de doubler l'apostrophe de la valeur), la valeur "John" viendra remplacer le deuxième ?, etc.

La notation est semblable pour les requêtes de type "select", "insert", "update", "delete", etc.

Le terme prepared statement vient du fait que la requête en soi est devenue indépendante des valeurs. Elle a la même forme quelques soient les valeurs que l'on souhaite insérer dans la table. La requête peut être préparée, c'est-à-dire parsée une seule fois par le serveur de base de données et servir pour plusieurs opérations.

Syntaxe utilisant un map

L'idée du map de paramètre est semblable à celui de la table de paramètres, mais ici les paramètres sont nommés. Plutôt que de remplacer les valeurs "en dur" par un simple ?, on les remplace par des noms de propriétés et on passe en paramètre un map qui associe des valeurs à ces propriétés. Pour indiquer qu'un identifiant est une référence de propriété, on la préfixe avec un :. L'avantage ici est qu'une référence de propriété peut être reprise plusieurs fois dans la requête.

Voici un exemple. En variante par table, la requête suivante oblige à passer plusieurs fois une même valeur dans la table de valeurs1:

1
2
3
4
5
6
$sql.select(`select idDemande, resume from Demande where idResponsable = ?
             union
             select idOffre, resume from Offre where idResponsable = ?
             union
             select idFacture, resume from Facture where idResponsable = ?`,
            [ 123, 123, 123 ]);

Le passage de paramètre par map permet d'éviter cette contrainte:

1
2
3
4
5
6
$sql.select(`select idDemande, resume from Demande where idResponsable = :user
             union
             select idOffre, resume from Offre where idResponsable = :user
             union
             select idFacture, resume from Facture where idResponsable = :user`,
            { user: 123 });

Passage de tableaux de valeurs
En plus d'éviter les répétitions de mêmes valeurs, la syntaxe autorise également le passage de tableaux de valeurs, ce qui est pratique par exemple pour les clauses IN.

En effet, la clause IN est contraignante car elle impose une énumération des valeurs. Cela complique sensiblement la syntaxe. Lorsqu'on passe des valeurs par map, le moteur se charge d'adapter la requête automatiquement. Ainsi, à la place de la requête

1
2
3
4
$sql.select(`select id, nom, prenom
             from Personne
             where id = ? and statut in (?, ?, ?)`,
            [ #idPersonne, statut[0], statut[1], statut[2] ]);

on pourra se contenter de

1
2
3
4
$sql.select(`select id, nom, prenom
             from Personne
             where id = :id and statut in (:status)`,
            { id: #idPersonne, status: statut });

Optimisations

Un prepared statement est réutilisable : il donne le gabarit de requête pour le traitement et peut servir pour plusieurs jeux de données. Ewt propose plusieurs variantes pour réutiliser un prepared statement.

Variante 1 - réutilisation simple d'un prepared statement
L'idée ici est de pré-générer un prepared statement et de le réutiliser ensuite pour passer les requêtes. L'avantage est que le SGBD n'a pas besoin de parser la requête à chaque exécution, ce qui, en théorie, représente un gain de temps. Dans la pratique le gain de temps est relativement minime avec cette méthode car le rapport entre le temps de parsing de la requête est bien moins coûteux que le temps d'exécution de cette dernière.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// on déclare un prepared statement et on reçoit alors
// un identifiant pour ce prepared statement
var pstmt = $sql.prepareStatement(`update Vendeur
                                   set Nom=?, Prenom=?
                                   where idVendeur=?`::T);

// on peut réutiliser cet identifiant dans les requêtes
$sql.update(pstmt, [ "César", "Jules", 14 ]);
$sql.update(pstmt, [ "Doe", "John", 30 ]);
$sql.update(pstmt, [ "Bernasconi", "Mario", 50 ]);

// on libère le prepared statement en fin de traitement
$sql.releaseStatement(pstmt);

Variante 2 - regroupement des données
Cette variante est très proche de la précédente. Elle consiste simplement à regrouper les 3 appels $sql.update en un seul. Les données sont alors regroupées dans une matrice organisée comme un tableau de tableaux:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// on déclare un prepared statement et on reçoit alors
// un identifiant pour ce prepared statement
var pstmt = $sql.prepareStatement(`update Vendeur
                                   set Nom=?, Prenom=?
                                   where idVendeur=?`::T);

// on peut réutiliser cet identifiant dans les requêtes
$sql.update(pstmt, [ [ "César", "Jules", 14 ],
                     [ "Doe", "John", 30 ],
                     [ "Bernasconi", "Mario", 50 ] ]);

// on libère le prepared statement en fin de traitement
$sql.releaseStatement(pstmt);

Les requêtes d'update retournent généralement le nombre de lignes modifiées. Ici les réponses sont placées dans un tableau de valeurs, où chaque entrée correspond à une exécution de requête.

Variante 3 - les batchs
Les variantes précédentes utilisent le principe de l'auto-commit. Cela signifie que chaque requête "agit" sur la base de données. Cela est utile si une requête dépend du résultat de la requête précédente.

Toutefois, si le but est de passer des requêtes en série et que ces requêtes n'ont pas de lien entre elles, il est possible de désactiver cet auto-commit pour le lot de requêtes, et de faire exécuter ces dernières en batch.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// on déclare un prepared statement et on reçoit alors
// un identifiant pour ce prepared statement; à noter
// l'option "autocommit: false" qui demande de désactiver
// l'auto-commit.
var pstmt = $sql.prepareStatement(`update Vendeur
                                   set Nom=?, Prenom=?
                                   where idVendeur=?`::T,
                                  { autocommit: false });

// on pousse les données sous forme de batch
$sql.addBatch(pstmt, [ "César", "Jules", 14 ]);
$sql.addBatch(pstmt, [ "Doe", "John", 30 ]);
$sql.addBatch(pstmt, [ "Bernasconi", "Mario", 50 ]);

// on lance l'exécution de la requête sur chaque élément de batch
// toutes les requêtes sont passées "one shot" et enregistrées en
// db via une unique commit
$sql.executeBatch(pstmt);

// on libère le prepared statement en fin de traitement
$sql.releaseStatement(pstmt);

L'autre avantage ici est que si une requête

Variante 4 - regroupement de batchs
La variante 4 est l'équivalent de la variante 2, mais appliquée aux batchs. L'idée est de regrouper les différents appels à $sql.addBatch en un seul, en passant alors une matrice de valeurs.

L'exemple est très similaire à celui de la variante 3. La seule différence se situe au niveau des 3 appels $sql.addBatch, qui sont regrpoués en un seul:

1
2
3
$sql.addBatch(pstmt, [ [ "César", "Jules", 14 ],
                       [ "Doe", "John", 30 ],
                       [ "Bernasconi", "Mario", 50 ] ]);

Comparatif des performances
Les 4 variantes ont été évaluées avec une même requête d'insertion répétée 20'000 fois.

version de base 11'929 ms
variante 1 11'318 ms
variante 2 11'952 ms
variante 3 1'207 ms
variante 4 1'110 ms

  1. Cette requête n'est pas optimale, mais elle est donnée pour l'exemple. Il serait bien entendu possible de la construire différemment (par exemple avec une clause with) pour se passer de répéter le paramètre.