Aller au contenu

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.

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 });

  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. ↩