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
@acceptou@rejectsur les scripts - appliquer les règles de sécurité au niveau du serveur d'application
(
web.xmlet autre)
Autres recommandations à mettre en œuvre
- Éviter d'utiliser
disable-output-escaping="yes"etxsl:copy-ofau 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 desanitize. - 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-Policydoit être utilisé avec la valeurno-referrer,same-originoustrict-origin-when-cross-originX-Frame-Optionsdoit être utilisé avec le paramètre adéquat suivant l’application (en principesameorigin)Access-Control-Allow-Originne 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 headerne 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
nullcomme 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
/webvia une requête GET sont ignorées (sauf si l'on active le paramètreallow-action-on-getau niveau du servlet dans le fichierweb.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 | |
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 | |
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 | |
Le passage de paramètre par map permet d'éviter cette contrainte:
1 2 3 4 5 6 | |
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 | |
on pourra se contenter de
1 2 3 4 | |
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 | |
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 | |
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 | |
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 | |
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 |
-
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. ↩