Sécuriser une URL (la Querystring) ou comment protéger les paramètres d'une modification de l'utilisateur ?
Préambule
Nous avions vu dans ce billet comment crypter la totalité des paramètres passés en GET (ie: en Querystring) afin d'empêcher toute visibilité sur ces derniers (par mesure de confidentialité, ou d'éviter toute modification).
Aujourd'hui, nous allons appliquer une autre méthode pour se protéger d'une modification des paramètres : vérification grâce à un HASH que les paramètres n'ont pas été modifiés entre la réception d'un URL et son affichage sur le navigateur. Le but est d'obtenir une URL du type : http://supersite.fr/?uid=3&myparam=monpara&HASH=monhash.
Le sujet sera traité en .NET / C#, à l'aide de 2 méthodes utilitaires qui nous serviront à créer une URL, et l'autre d'opérer une vérification sur celle-ci.
Objectif
Grâce aux paramètres d'une querystring candidate (param1=val1¶m2=val2&...¶mN=valN), nous allons créer un HASH (SHA256), HASH qui sera passé dans cette même querystring, il nous restera plus qu'à recalculer le HASH des paramètres d'arrivée afin de le comparer avec le HASH précédemment calculé.
On pourrait appliquer ce principe pour créer un HASH de mots de passe par exemple, afin de ne pas avoir de mots de passe en clair.
Comment faire ?
Nous avons besoin des éléments suivants :
- d'une chaîne qui sera transformée en HASH, pour cela, celle-ci sera composée de 2 parties concaténées :
- d'une partie publique = paramètres de l'URL
- d'une partie privée = une chaîne contenue dans un fichier de configuration, dans un champ de table, bref, quelque part
- d'une foncion de HASH : SHA256 (MD5 ayant été crackée, on évitera)
- d'une méthode qui me HASH la chaîne et me renvoie l'URL à fournir
- d'une méthode, qui à partir d'une URL, vérifie la validité de cette dernière
Code des méthodes utilitaires
namespace Util { public class Securite { /// <summary> /// Création d'une querystring : ?para1=val1¶2=val2&...¶N=valN /// </summary> /// <param name="qs">la querystring créée</param> /// <returns></returns> private static string _getQS(NameValueCollection qs) { foreach (string k in qs) sb.AppendFormat("{0}={1}&", k, qs[k]); sb.Remove(sb.Length - 1, 1); return sb.ToString(); } /// <summary> /// Création d'une URL avec un HASH des paramètres /// pour vérification future des paras. /// </summary> /// <param name="qs">QueryString</param> /// <returns></returns> public static string createQSSecure(NameValueCollection qs) { string cle = ConfigurationManager.AppSettings["cryptKey"]; string hash = string.Empty; string qsStr = string.Empty; qs2.Remove("Hash"); qsStr = _getQS(qs2); hash = getHashSHA256(string.Format("{0}{1}", cle, qsStr)); // protection des caractères réservés pour l'URL hash = hash.Replace("/", "_").Replace("+", "-"); return String.Format("{0}&Hash={1}", qsStr, hash); } /// <summary> /// Vérifie à partir d'une collection de paramètres (dont un Hash) /// s'il ont été modifié (comparaison de Hash créé précédemment et du /// Hash créé avec qs) /// </summary> /// <param name="qs">QueryString</param> /// <returns></returns> public static bool isUrlSecureValid(NameValueCollection qs) { string cle = ConfigurationManager.AppSettings["cryptKey"]; string hash = qs["Hash"]; // on remet au format original les caractères protégés hash = hash.Replace("_", "/").Replace("-", "+"); qs2.Remove("Hash"); return hash.Equals(getHashSHA256(string.Format("{0}{1}", cle, _getQS(qs2)))); } /// <summary> /// calcul le hash SHA256 d'une clé /// </summary> /// <param name="phrase"></param> /// <returns></returns> public static string getHashSHA256(string phrase) { byte[] data = Encoding.UTF8.GetBytes(phrase); { byte[] encryptedBytes = sha.TransformFinalBlock(data, 0, data.Length); return Convert.ToBase64String(sha.Hash); } } } }
Exemple d'utilisation
Pourquoi faire ce type d'URL ? par exemple, nous souhaitons mettre en place une réinitialisation de mot de passe afin d'éviter de l'envoyer en clair, le HASH permettrait d'éviter toute modification des paramètres de l'URL envoyée par mail.
Ajout d'un token
On pourrait également ajouter une clé générée (on pourrait aussi utiliser la méthode suivante pour générer des mots de passe) dynamiquement à chaque utilisation de l'URL, en injectant celles-ci dans les paramètres, dès lors l'URL envoyée par mail est unique et sécurisée, on complexifie ainsi toute tentative de décryptage de la clé de HASH pour chaque URL générée, elle ressemblera alors :
idUser=18759&context=reinit&token=BQ2drUwm&Hash=Mr07G4NaiB3B9UtnMdmZRA26IBTp4Kyrgrur3FT6XlI=
on pourra alors utiliser cette ligne dans une URL, par exemple
http://monsupersite.fr/pwdReinit.aspx?idUser=18759&context=reinit&token=BQ2drUwm&Hash=Mr07G4NaiB3B9UtnMdmZRA26IBTp4Kyrgrur3FT6XlI=
Trouvée sur Google code, une méthode qui génére des clés uniques (comme le ferait GUID) : http://www.google.fr/search?q=RNGCryptoServiceProvider, bien plus courtes et simples.
/// <summary> /// Génération de clés uniques /// </summary> /// <param name="maxsize"></param> /// <returns></returns> public static string getUniqueKey(int? maxsize) { int PasswordLength = maxsize ?? 8; String _allowedChars = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ23456789"; rng.GetBytes(randomBytes); int allowedCharCount = _allowedChars.Length; for (int i = 0; i < PasswordLength; i++) chars[i] = _allowedChars[(int)randomBytes[i] % allowedCharCount]; }
Utilisons nos méthodes pour générer les paramètres d'une page qui sera envoyée par mail à un utilisateur, cette page vérifiera si les paramètres ont été ou non modifiés. On pourrait mettre comme paramètre à createQSSecure() directement Request.QueryString si besoin.
Page qui génére l'URL
namespace myWebApp { public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { myparas.Add("idUser", "18759"); myparas.Add("context", "reinit"); myparas.Add("token", Securite.getUniqueKey(null)); string myparams = Securite.createQSSecure(myparas); // suite des traitements, envoi d'une URL avec les params myparams } } }
Page qui réceptionne les paramètres précédemment fixés et les vérifie
namespace myWebApp { public partial class pwdReinit : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (!Securite.isUrlSecureValid(Request.QueryString)) // suite des traitements avec les paramètres passés } } }
Attaché (Annexes) au billet, les sources du cas pratique.