Sécurisation d'une session - PHP - Programmation
Marsh Posté le 27-02-2005 à 22:00:22
A 1ere vue ça me semble qu'au lieu de securiser 1 truc tu ajoutes 2 autres points vulnerables au systeme ... Parcontre faut envoyer le Sid par cookie au client, ça c'est imperatif ...
Parcontre ca m'etonnerais pas que phpbb utilise un truc du genre vu le nombre de prob de secu qu'ils ont
Marsh Posté le 27-02-2005 à 22:07:21
oui enfin je voulais dire que je ne créais pas moi meme explicitement de cookies de session, a priori c'est php qui le génère tout seul il me semble non?
Et la on me conseille de faire une session, + créer un cookies, + écrire la session en dure dans la base.
A priori faire 3 vérifications est plus sécurisée que une seule vérif dans l'absolu non?
sinon les questions de mon premier post restent d'actualité
Marsh Posté le 27-02-2005 à 22:37:22
Je n'utilises pas le systéme de session de php mais je vérifies l'identifiant envoyé par le navigateur (contenu dans le cookie) et l'adresse IP du demandeur. Ca limite déjà grandement les risques. Si un jour j'avais besoin de plus de sécurité, je rajouterais l'identifiant du navigateur voire une complication de l'identifaint de session par un rajout de plusieurs caractéres aléatoires.
Ca m'étonerait vraiment que les gas qui ont écrit le systéme ne vérifient pas au moins les deux premiéres chôses.
Marsh Posté le 27-02-2005 à 22:45:04
Bon deja ton "triple protocole d'identification : un par session_id, un par cookies, et un par stockage en dure de la session dans la base de données." Ne veut rien dire.
Il existe deux gros "problemes" lies au sessions et au systeme de "relogin" automatique.
Le premier probleme est le fait que les sessions sont stockees sur le disque dur du serveur. Mais que ces dernieres sont accessibles aux autres sites qui sont sur la meme machine. En effet le repertoire ou sont stockees les sessions est commun a tous les sites (dans la plupart des cas). |
Afin de "boucher" ce trou, une des solutions retenues la plupart du temps, consiste a stocker les sessions dans ta BDD. Comment ? En creant ton propre "gestionnaire de sessions" que tu vas ensuite declarer a php. Php utilisera ensuite ce gestionnaire pour ton site. Donc grace a ce gestionnaire tu pourras faire ce que tu veux de tes sessions, les stocker dans une BDD, les stocker dans un repertoire specifique a ton site, les crypter, en bref, tu as total control dessus.
Le deuxieme probleme concerne l'utilisation du systeme de "relogin" automatique. Avant de venir au coeur du probleme, petite explication sur le processus d'autentification. |
Malheureusement je ne connais pas de technique efficace pour contrer cette "attaque" si ce n'est "blinder" ses script php ou bien ne pas proposer de "relogin automatique".
Il existe egalement un autre probleme. Il existe des script de "brute force" pour essayer de trouver le password de quelqu'un. Ces script ne font qu'essayer des password les uns apres les autres pour essayer de trouver le bon password. Certes c'est long, mais cela peut fonctionner. D'ailleurs il suffit que le script soit un peu plus evolue, par exemple un qui utilise des mots du dicctionnaire et que la personne possede un password simple pour que le script arrive a casser le password |
Pour ce genre d'attaques, il y a plusieurs solutions.
Limiter le nombre d'essais dans un laps de temps. Par exemple pas plus de 5 essais en moins de 5 minutes. Cela ne risque pas de gener quelqu'un qui essaye de se rappeler son passoword, mais cela ralentira assez un script.
On peut egalement dire aux utilisateurs d'utiliser des password compliques, mais helas, la plupart n'ecouteront pas.
Il existe encore une autre technique peu connue (en tout cas je l'ai jamais vue mise en oeuvre). Elle se base sur un "proof of concept" que j'ai vu il y a quelques mois.
La technique se base sur la "biometrie". Vous savez ces appareils qui lisent les empreintes, la retine et compagnie. Vous allez me dire que ce genre d'appareils ne se trouvent pas au coin de la rue. Et pourant je vais vous etonner.
La biometrie consiste a "lire" des characteristiques propres a un individue. Cela la plupart du temps, se traduit par l'utilisation d'appareils a la pointe de la technologie... Pourtant, de tels appareils ne sont pas necessaires.
Il existe un moyen pour transformer n'importe quel clavier ordinaire en un appareil biometrique. Le principe de base va etre de mesurer le temps que mets l'utilisateur a taper les differentes lettre de son password. Apres un certain temps "d'apprentissage" le script sera cappable de dire si le password rentre etait ecrit par la personne a qui il appartient ou a une autre personne.
Quand nous entrons un password au clavier, On ne tappe pas a la meme vitesse les characteres. Par exemple, si l'on prend un mot "javelot" (il necessite les deux mains).
Personnellement je tape "ja" tres rapidement, ensuite la prochaine letre "v" est un peu plus lente. Puis viens "e" encore un plus lente (meme main). Ensuite "lo" sont assez rapides pour finir avec le "t".
Il suffit que le script "mesure" l'intervale entre chaque lettre un certain nombre de fois pour qu'il puisse en tirer une "moyenne" ainsi que des valeurs maximales et minimales.
Ensuite grace a l'utilisation de javascript (je sais, c'est mal), on va pouvoir mesurer le temps lors de la saisie du password. Ces valeurs seront ensuites transmises avec le password. Le script php se chargera ensuite de verifier de un le password, et si ce dernier est correct de verifier les valeurs numeriques qui correspondent aux intervalles des touches. Si ces valeurs rentrent dans les valeurs de tolerance, alors il y a de fortes chances que ce soit la bonne personne.
Evidement cette technique n'est pas parfaite :
-Utilisation de javascript
-Phase d'aprentissage pendant laquelle cette "protection" est desactivee.
-Lors du changement de password, la phase d'apprentissage doit de nouveau avoir lieu.
En revanche elle a le merite d'etre protegee contre les script de "brute force" ou ceux qui utilisent un dictionnaire.
On peut debatre sur l'utilitee de cette methode si les valeurs des intervalles sont "compromises". En effet, un pirate pourrait adapter son script pour que ce dernier "emule" la frape au clavier. Mais pour que cela fonctionne il faut que le pirate connaisse le password, or si le password a ete cryptee dans la BDD, cela est dur. Certes il peut utiliser ces valeurs dans un script "brute force", mais cela va ralentir le script (et si on plus on a prevu des limites, alors le script sera encore plus ralenti).
Voila, j'espere avoir reussi (avec cette longue tirade) a vous montrer que la securitee n'est jamais absolue, qu'il existe des trous, des rustines, mais que rien n'est parfait dans ce bas monde
Marsh Posté le 27-02-2005 à 23:00:50
Pour la biométrie par mesure de la vitesse de frappe de chaque mot :
Ces différents événements risquent de faire sortir des intervales de mesures autorisé par le script.
Ce n'est donc pas, en pratique, une bonne méthode. C'est donc une fausse bonne idée de sécurisation.
EDIT: Et en plus, il faudrait trouver un langage accepté par tout le monde dans leur navigateur et qui permette de mesurer des intervales de saisies, c'est pas gagné.
Marsh Posté le 27-02-2005 à 23:15:00
@Cerel : merci beaucoup pour cette réponse très précise.
Visiblement, puisque je suis mon propre hébergeur, l'utilisation d'une session sans proposition de login automatique semble être une bonne solution d'après ce que tu me dis.
- Par contre que veux-tu dire par "blinder" ses scripts pour protéger les cookies de reconnection automatique?
Sinon, lorque j'utilise une session, je stocke dans $_SESSION['password'] la réelle valeur du password, et non sa valeur cryptée.
- Est-ce que cela pose un problème de sécurité? Il ne me semble pas puisque cette variable est stockée coté serveur et donc pas réechanger à chaque fois avec l'utilisateur, mais j'aimerais confirmation svp
questions subsidaires :
- mais alors quels sont donc ces pbl de hack de variables de session réalisé par des bots ? je ne vois pas où cela pourrait intervenir par rapport aux réponses de Cerel
- et quel intéret de stocker les variables de session dans ta BDD, alors que le serveur le fait dejà pour lui-meme?
encore merci
Marsh Posté le 27-02-2005 à 23:40:19
Pour les bruteforce, la meilleure maniere a mon avi, celle que j'utilise et qui est utilisée par ssh, c'est de faire un systeme avec temps exponentiel. En gros 1ere erreur => t'attend 30 sec, 2ème => t'attend 2 min, 3ème => 10min , 4=> 2h ,... Avec un bruteforce qui fait 10'000 erreurs a la seconde, il est bon pour etre banni pendant les 500 prochains millenaires
Marsh Posté le 28-02-2005 à 00:38:15
Djebel1 a écrit : @Cerel : merci beaucoup pour cette réponse très précise. |
Ce que je veux dire c'est qu'il faut faire en sorte de proteger au maximum les formulaires de saisie, et toutes les variables GET et POST, afin d'eviter qu'un eventuel pirate n'arrive a faire un attaque de type "SQL Injection".
Djebel1 a écrit : Sinon, lorque j'utilise une session, je stocke dans $_SESSION['password'] la réelle valeur du password, et non sa valeur cryptée. |
A quoi ca te sert de stocker le password ? Et en clair qui plus est ...
Djebel1 a écrit : |
Je ne suis pas sur a quoi tu fais reference, donc je ne peux pas trop repondre a ce sujet. Aurais-tu plus d'infos ?
Djebel1 a écrit : |
Le serveur stocke les sessions dans un repertoire accessible a tout le monde. Le fait de les stocker dans ta BDD va empecher tes sites "voisins" d'y acceder. Tu peux egalement crypter les sessions si tu en as envie, et ca php ne le fait pas de base, mais si tu cree ton propre gestionnaire de sessions, tu pourras le faire.
Djebel1 a écrit : encore merci |
Faut surtout que tu te rende comptes que rien n'est a 100% securise.
Dans toute la chaine pour le login, il peut toujours y avoir un maillon insoupçone qui lache, et ce n'est pas toujours le meme...
Par exemple, il peut y avoir une faille sur le serveur mail installe sur la meme machine, cette faille peut conduire a l'execution d'un code arbitraire qui permet au pirate d'executer un "root kit", il aura ainsi un access admin au serveur.
Il peut par exemple y avoir une faille dans php qui fait qu'apache va simplement "afficher" le code php sans "l'executer". Du coup le pirate pourra voir le code source, l'analyser et en sortir les failles beaucoup plus facilement.
Marsh Posté le 28-02-2005 à 01:24:02
cerel a écrit : Ce que je veux dire c'est qu'il faut faire en sorte de proteger au maximum les formulaires de saisie, et toutes les variables GET et POST, afin d'eviter qu'un eventuel pirate n'arrive a faire un attaque de type "SQL Injection". |
tu veux dire faire de bonnes vérifications au niveau de ce qui est entré par l'utilisateur? du genre restreindre certains symboles etc? (enfin si cette question te saoule j'irai me renseigner sur le "SQL injection" )
cerel a écrit : A quoi ca te sert de stocker le password ? Et en clair qui plus est ... |
il s'agit d'une interface PHP réservé à un groupe d'utilisateurs restreints (définis par leurs login et pass MySQL) afin d'entrer dans la base elle-même de nouvelles données, j'utilise donc un mysql_connect avec les $_SESSION['login'] et $_SESSION['password']. S'il y a un autre moyen je le connais pas
[EDIT]ou alors, en définissant une autre table de log/pass dans la base, et si l'utilisiteur est identifié dans cette table, alors le script lui confere les droits MySQL d'insérer des données dans la base. Dans ce cas plus besoin de stocker le pass en clair dans une variable de session, mais un password MySQL sera indiqué en clair sur le code source PHP. Je ne sais pas lequel des 2 est le plus sécurisé, si jamais tu en as une idée
[/EDIT]
cerel a écrit : Je ne suis pas sur a quoi tu fais reference, donc je ne peux pas trop repondre a ce sujet. Aurais-tu plus d'infos |
non, je discutais avec quelqu'un qui gère un forum PHPBB. Après avoir expliqué que sa table de session avait explosé car site trop fréquenté, je lui ai demandé à quoi servait de stocker dans la BDD les sessions, il m'a répondu que c'était facilement hackable par des bots, ca doit rejoindre ton problème d'enregistrement des sessions sur un répertoire commun
En conclusion, la solution de passer uniquement par les variables de session, sachant que je suis mon propre hébergeur (donc pas de partage de session), malgré le fait que l'une d'entre-elle stocke le password en clair, cela te parait-il relativement satisfaisant?
Marsh Posté le 28-02-2005 à 20:50:24
Djebel1 a écrit : tu veux dire faire de bonnes vérifications au niveau de ce qui est entré par l'utilisateur? du genre restreindre certains symboles etc? (enfin si cette question te saoule j'irai me renseigner sur le "SQL injection" ) |
Oui en effet, il faut controler ce que rentre l'utilisateur. Donc faire attention aux ' et aux ", mais egalement au type de donnees. Si tu as un formulaire ou l'utilisatuer doit rentrer un chiffre, mais que tu recois du texte ...
Si tu veux "bien" faire, faut prendre comme "hypothese de depart" que le mec est un pirate. Donc tu dois absolument tout controler. Autre hypothese sympa "si l'utilisatuer peut faire une connerie, alors il la fera".
Djebel1 a écrit : |
Bon cela m'a l'air d'un cas exceptionnel. Donc a la limite, dans ce cas la ok, tu pourrais stocker le pass en clair (car du dois le reutiliser). Mais faut eviter que cela ne devienne une habitude
Djebel1 a écrit : |
J'ai pas d'infos a ce sujet, desole
Djebel1 a écrit : |
Si tu es ton propre hebergeur, alors tu elimines certes un trou potentiel. Mais il ne faut pas te croire a l'avris... Il peut y avoir un trou dans un autre programme installe sur ta machine que tu as oublie de patcher...
Pour finir, oui l'utilisation d'une session comme tu l'a decrites pourrait tenir la route, mais je te conseille quand meme de te renseigner sur la creation de ton gestionnaire de sessions afin de stocker les sessions dans ta BDD.
Et puis meme si les utilisateurs doivent rentrer un login valide de la BDD, rien ne t'empeche d'avoir un autre login pour les access a la base qui ne dependent pas de l'utilisateur logue.
Marsh Posté le 28-02-2005 à 23:22:47
ok merci bcp pour tous ces conseils
cerel a écrit : Et puis meme si les utilisateurs doivent rentrer un login valide de la BDD, rien ne t'empeche d'avoir un autre login pour les access a la base qui ne dependent pas de l'utilisateur logue. |
un peu comme je le proposais dans mon [edit] non? mais dans ce cas le log et pass pour les acces à la base sont stockés en clair dans la source PHP, je sais pas ce qui est le mieux. Bon allez je m'en vais potasser tout ca
Marsh Posté le 04-03-2005 à 16:16:06
cerel a écrit :
|
J'ai utilisé sur un de mes sites une méthode qui permet au moins d'éviter ce problème.
Lorsque l'utilisateur s'est connecté avec succès en utilisant son login et son mot de passe, il se voit attribuer un numéro de ticket (chaîne de caractères longue et aléatoire, assez longue pour être supposée impossible à deviner).
Ce numéro de ticket est stockée dans un cookie de login automatique. En base de données, on ajoute un enregistrement qui associe le ticket à un utilisateur (ainsi qu'un hash de son IP et un hash de son useragent).
Lorsqu'un utilisateur non connecté revient sur le site:
- je purge les vieilles infos de connexion automatique
- je regarde s'il a un cookie de connec automatique
- je vérifie si le numéro de ticket est valide et que l'IP et l'UA concordent (vu que
l'information provient d'un cookie, il n'y a pas de raison que l'UA ait changé)
- si tout est bon je log le mec
C'est toujours contournable, mais un plus pénible à contourner qu'une simple attaque par piquage de hash de password.
Marsh Posté le 04-03-2005 à 16:29:53
oui mais si le pirate a acces au hash du password par SQL injection, il doit avoir acces au reste pitetre? (je me susi pas encore documenté sur la SQL injection )
SI il a acces au reste, il doit pas avoir de mal a récupérer l'IP et l'UA dans la base j'imagine (enfin comme d'hab, mes dires se basent sur du vent )
Marsh Posté le 04-03-2005 à 16:50:58
C'est pas dur de se protéger contre les injections SQL quand même
Marsh Posté le 04-03-2005 à 16:58:16
Djebel1 a écrit : oui mais si le pirate a acces au hash du password par SQL injection, il doit avoir acces au reste pitetre? (je me susi pas encore documenté sur la SQL injection ) |
Une fois qu'il a le ticket, le hash de l'IP et le hash de l'UA, il fait quoi l'attaquant ?
Il n'a pas idée de la fonction de hash utilisée. Pour faire chier j'ai très bien pu utiliser un md5 maison. Dans ce cas il ne retrouvera jamais la bonne IP, ni un UA compatible. Le script va le jeter s'il forge un cookie d'autoconnexion et qu'il tente son attaque.
Deuxième point, le ticket est invalidé s'il y a tentative d'autologin avec IP/UA qui ne concordent pas. Résultat, il n'a plus qu'à attendre que l'utilisateur légitime se reconnecte, à ré-exploiter son injection SQL, et... retenter sa manip.
Marsh Posté le 04-03-2005 à 17:17:36
ratibus a écrit : C'est pas dur de se protéger contre les injections SQL quand même |
Va le dire a phpbb
Kriscool : Le probleme c'est qu'une ip sa change plus qu'un UA, enfin ca depends de la connexion. Et un UA ca peut se trafiquer.
Marsh Posté le 04-03-2005 à 17:38:28
cerel a écrit : Va le dire a phpbb |
Oui pour les deux.
L'UA ça peut se trafiquer, mais si tu dois trouver l'UA qui une fois passé à la moulinette d'une fonction de hash que tu ne connais pas, doit donner une chaîne que tu connais, bah je dirais que tu est bien avancé
Marsh Posté le 04-03-2005 à 21:03:51
Faut deja passer tout ce qui rentre au addslashes, ou a la limite j'ai vu une fonction ... je sais plus le nom... qui rend une chaine conforme au format SQL ... Donc a premiere vue ca peut securiser contre les injections SQL, du moins le genre qu'on presente sur php.net
Marsh Posté le 05-03-2005 à 07:22:32
Mieux vaut prevoir un script qui marchera avec ou sans magic-quote que faire un site moulé sur une config
Marsh Posté le 05-03-2005 à 10:42:21
ou alors changer la conf des magic quotes via un ini_set... ça t'évite de foutre des addslashes partout mais tu est ok sur toutes les confs paske tu les adapte à ton script
Marsh Posté le 08-03-2005 à 10:17:06
esox_ch a écrit : Faut deja passer tout ce qui rentre au addslashes, ou a la limite j'ai vu une fonction ... je sais plus le nom... qui rend une chaine conforme au format SQL ... Donc a premiere vue ca peut securiser contre les injections SQL, du moins le genre qu'on presente sur php.net |
Xav_ a écrit : ou alors changer la conf des magic quotes via un ini_set... ça t'évite de foutre des addslashes partout mais tu est ok sur toutes les confs paske tu les adapte à ton script |
Pour être plus adapté à toutes les confs, je pense que le mieux est d'utiliser des objets "adaptateurs", vous accéder à sa base de données où aux autres fonctions sensibles.
Pour mon site, je ne fais aucune requête directement. J'ai recodé un truc similaire aux "prepared statements" de PHP 5. Dans mon code, mes requêtes sont du genre 'SELECT * FROM %t WHERE champ=%i', et j'utilise une fonction de remplacement pour composer la requête, en échappant proprement les paramètres.
Ca me permet de faire les adaptations suivantes, selon le besoin
- transformation d'une valeur null en chaîne 'NULL'
- transformation d'une chaîne vide en chaîne 'NULL'
- vérification qu'un entier est bien un entier
- échappement des chaînes de caractères
- conversion de dates
- etc.
Une sorte de printf quoi.
Via cette classe, je peux également tenir un "log" des requêtes effectuées, ce qui aide bien au débug, et faire encore pas mal de choses dans le style.
Après il me suffit de n'utiliser que cette classe pour les requêtes, ce qui me permet:
- de ne plus avoir à me préoccuper de la connexion à la base
- de ne plus avoir à faire attention aux valeurs des variables que j'inclus dans mes requêtes
De plus, s'il s'avère que j'ai omis une vérification de sécurité sur un type de variables dans ma classe, je n'ai que cette classe à modifier.
Maintenant, c'est ma façon perso d'avoir fait les choses. Entre autres parce que je n'avais pas accès à PEAR DB. Si c'était à refaire, je me taperais pas tout moi même à la main
Marsh Posté le 08-03-2005 à 10:18:46
Je viens de retrouver le nom de la fonction :
mysql_real_escape_string() , ça a l'air pas mal
Marsh Posté le 21-03-2005 à 01:49:41
lu tlm!
juste 2-3 ptites choses basées sur ces interventions intéressantes:
- même via un init_set(), est-ce que ça marche bien le magic_quote ? Je connais que le côté "applicatif" (le front-office n'est-ce pas..) d'Apache, et du coup g po forcément confiance, mais je sais po si c'est à tort ou à raison
- @Kriscool: de toute facon la vérification de cohérence & d'intégrité des données doit toujours se faire avant insertion dans la base.. Là tu l'as faite en objet, c'est sûr que c'est + pratique et + beau Ou alors j'ai po bien compris
- @esox_ch: mysql_real_escape_string() Juste: ça prend en compte le jeu de caractères courant.. Ya po de risque de pouvoir créer des correspondances avec d'autres caractères si jamais on modfiait ce jeu
Sinon c intéressant les sujets sur la sécurité (surtout logicielle), ça rassure un peu..
Marsh Posté le 21-03-2005 à 08:08:34
>LKoLRn : Le ini_set() n'a pas d'influence sur le magic_quote malgres ce que beaucoup de personnes pensent, va voir sur www.php.net et tu le verras.
Et je comprends pas ce que tu veux dire par ton truc des caracteres
Marsh Posté le 21-03-2005 à 14:28:59
lkolrn a écrit : - @Kriscool: de toute facon la vérification de cohérence & d'intégrité des données doit toujours se faire avant insertion dans la base.. Là tu l'as faite en objet, c'est sûr que c'est + pratique et + beau Ou alors j'ai po bien compris |
C'est pas plus beau, c'est juste ma méthode à moi pour grouper les différentes vérifications à un seul endroit bien défini du code, pour pas avoir à me les retaper partout.
Marsh Posté le 21-03-2005 à 18:06:53
ui, c de l'objet...
@esox_ch: cette fonction avec les directives magic_quotes_* ne fonctionne po Faut forcément aller modifier le fichier en dur alors ??
ps: pour le jeu de caractères, g dis une connerie..
Marsh Posté le 21-03-2005 à 20:52:53
lkolrn a écrit : ui, c de l'objet... |
Exact, ou alors tu fais ca proprement => tu fais une fonction qui detecte si c'est on ou off et strip/add les slashs en consequence
Marsh Posté le 22-03-2005 à 07:54:37
Un truc dans le genre :
Code :
|
Marsh Posté le 29-03-2005 à 17:18:41
Par rapport à la méthode que j'utilise pour identifier les utilisateurs (traités plus haut donc), je voulais votre opinion sur une solution alternative, pour savoir ce qui d'après vous est le "moins pire" :
- Solution actuellement utilisée : étant donné que les utilisateurs doivent pouvoir modifier la base SQL en elle-même, leurs logins et pass sont ceux de la base elle-même. Le pass est stocké en clair dans une variable de session, afin qu'un utilisateur identifié puisse faire les insert, delete, update, etc.
- Solution alternative : je peux également utiliser une table de logins dans la base de données ; quand un utilisateur se connecte, on va voir dans cette table si son login et pass sont valides (jusque la classique) et on vérifie son niveau de privilèges.
En fonction de son niveau de privilège, le script PHP utilisera alors un login et pass adapté, et contenu dans le script.
Concrètement on aurait donc une série de logins et pass dans le script PHP.
D'après vous quelle est la meilleure solution? (bien sur si vous en avez d'autres je prends ^^)
Marsh Posté le 27-02-2005 à 21:52:27
Bonjour, ce pti post pour un classique problème de sécurité lié aux sessions.
Pour identifier des utilisateurs et les laisser connectés tout au long de leur navigation j'utilisais tout simplement une session (sans possibilité de connection automatique c'était voulu).
On me dit que cette solution est peu sécurisée car facilement hackable meme par des bots. Et qu'il faut utiliser un triple protocole d'identification : un par session_id, un par cookies, et un par stockage en dure de la session dans la base de données. On m'a aussi dit que c'est comme ca que faisaient les forums en phpbb.
D'où mes questions :
en quoi l'utilisation de session seule est peu sécurisée?
que veut-on dire par stockage de la session dans la base? juste le session_id? toutes les infos de sessions?
Merci