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"
etxsl: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 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-Policy
doit être utilisé avec la valeurno-referrer
,same-origin
oustrict-origin-when-cross-origin
X-Frame-Options
doit être utilisé avec le paramètre adéquat suivant l’application (en principesameorigin
)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 |
|
<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="{"action":"script","params":{"name":"hacktest"}}"/>
<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 |
|
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. ↩