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)

Protection de base

CSRF est un type d'attaque permettant de lancer des commandes au serveur en utilisant la session d'un autre utilisateur. L'idée est de faire exécuter une requête pré-construite à un utilisateur déjà authentifié sur une application. Le serveur croit recevoir une requête légitime et effectue le traitement demandé. L'attaquant peut ainsi faire passer des commandes à l'insu de l'utilisateur, tel que des transactions ou des changements de mot de passe.

Au niveau des requêtes /web, le moteur est protégé contre CSRF du fait que :

  • chaque requête non GET doit fournir l'identifiant de session
  • l'identifiant de session est aléatoire et ne peut pas être deviné
  • les actions passées sur /web via une requête GET sont ignorées (sauf si l'on active le paramètre allow-action-on-get au niveau du servlet dans le fichier web.xml)

Au niveau des requêtes /rest, il est de la responsabilité des scripts de s'assurer que s'ils effectuent des modifications de valeur, ils sont évalués dans le cadre d'une requête persistante via l'annotation @accept(thread="useragent-persistent") ou la méthode de script $session.isPersistent. En effet les requêtes persistantes sont protégées contre CSRF pour les mêmes raisons qu'évoqué plus haut.

Protection avancée

Le moteur d'application s'appuie sur pac4j pour l'authentification. Cette librairie intègre également une protection contre CSRF à l'aide d'un token pac4jCsrfToken.

Pour activer la protection CSRF de pac4j, il faut ajouter l'authorizer csrfCheck au niveau du filtre de sécurité dans le fichier web.xml. Ce faisant, le module d'authentification s'attendra à recevoir un champ de formulaire ou un header HTTP nommé pac4jCsrfToken contenant la valeur du token. Cela nécessite donc une adaptation au niveau de l'application (ajout de champ de formulaire). La valeur du champ pac4jCsrfToken peut être obtenue soit via le cookie pac4jCsrfToken, soit via l'entrée /output/session/pac4jCsrfToken de l'arbre de sortie du moteur.

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.