Sécuriser une URL (la Querystring) ou comment protéger les paramètres d'une modification de l'utilisateur ?

Publié le 15 juin 2008 par Olivier Duval

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&param2=val2&...&paramN=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

  1. namespace Util
  2. {
  3. public class Securite
  4. {
  5. /// <summary>
  6. /// Création d'une querystring : ?para1=val1&para2=val2&...&paraN=valN
  7. /// </summary>
  8. /// <param name="qs">la querystring créée</param>
  9. /// <returns></returns>
  10. private static string _getQS(NameValueCollection qs)
  11. {
  12. StringBuilder sb = new StringBuilder();
  13. foreach (string k in qs)
  14. sb.AppendFormat("{0}={1}&", k, qs[k]);
  15. sb.Remove(sb.Length - 1, 1);
  16. return sb.ToString();
  17. }
  18. /// <summary>
  19. /// Création d'une URL avec un HASH des paramètres
  20. /// pour vérification future des paras.
  21. /// </summary>
  22. /// <param name="qs">QueryString</param>
  23. /// <returns></returns>
  24. public static string createQSSecure(NameValueCollection qs)
  25. {
  26. string cle = ConfigurationManager.AppSettings["cryptKey"];
  27. string hash = string.Empty;
  28. string qsStr = string.Empty;
  29. NameValueCollection qs2 = new NameValueCollection(qs);
  30. qs2.Remove("Hash");
  31. qsStr = _getQS(qs2);
  32. hash = getHashSHA256(string.Format("{0}{1}", cle, qsStr));
  33. // protection des caractères réservés pour l'URL
  34. hash = hash.Replace("/", "_").Replace("+", "-");
  35. return String.Format("{0}&Hash={1}", qsStr, hash);
  36. }
  37. /// <summary>
  38. /// Vérifie à partir d'une collection de paramètres (dont un Hash)
  39. /// s'il ont été modifié (comparaison de Hash créé précédemment et du
  40. /// Hash créé avec qs)
  41. /// </summary>
  42. /// <param name="qs">QueryString</param>
  43. /// <returns></returns>
  44. public static bool isUrlSecureValid(NameValueCollection qs)
  45. {
  46. string cle = ConfigurationManager.AppSettings["cryptKey"];
  47. string hash = qs["Hash"];
  48. // on remet au format original les caractères protégés
  49. hash = hash.Replace("_", "/").Replace("-"rotection des caractères réservés pour l'URL
  50. hash = hash.Replace("/", "_").Replace("+", "-");
  51. return String.Format("{0}&Hash={1}", qsStr, hash);
  52. }
  53. /// <summary>
  54. /// Vérifie à partir d'une collection de paramètres (dont un Hash)
  55. /// s'il ont été modifié (comparaison de Hash créé précédemment et du
  56. /// Hash créé avec qs)
  57. /// </summary>
  58. /// <param name="qs">QueryString</param>
  59. /// <returns></returns>
  60. public static bool isUrlSecureValid(NameValueCollection qs)
  61. {
  62. string cle = ConfigurationManager.AppSettings["cryptKey"];
  63. string hash = qs["Hash"];
  64. // on remet au format original les caractères protégés
  65. hash = hash.Replace("_", "/").Replace("-", "+");
  66. NameValueCollection qs2 = new NameValueCollection(qs);
  67. qs2.Remove("Hash");
  68. return hash.Equals(getHashSHA256(string.Format("{0}{1}", cle, _getQS(qs2))));
  69. }
  70. /// <summary>
  71. /// calcul le hash SHA256 d'une clé
  72. /// </summary>
  73. /// <param name="phrase"></param>
  74. /// <returns></returns>
  75. public static string getHashSHA256(string phrase)
  76. {
  77. byte[] data = Encoding.UTF8.GetBytes(phrase);
  78. using (HashAlgorithm sha = new SHA256Managed())
  79. {
  80. byte[] encryptedBytes = sha.TransformFinalBlock(data, 0, data.Length);
  81. return Convert.ToBase64String(sha.Hash);
  82. }
  83. }
  84. }
  85. }

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.

  1. /// <summary>
  2. /// Génération de clés uniques
  3. /// </summary>
  4. /// <param name="maxsize"></param>
  5. /// <returns></returns>
  6. public static string getUniqueKey(int? maxsize)
  7. {
  8. int PasswordLength = maxsize ?? 8;
  9. String _allowedChars = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ23456789";
  10. Byte[] randomBytes = new Byte[PasswordLength];
  11. RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
  12. rng.GetBytes(randomBytes);
  13. char[] chars = new char[PasswordLength];
  14. int allowedCharCount = _allowedChars.Length;
  15. for (int i = 0; i < PasswordLength; i++)
  16. chars[i] = _allowedChars[(int)randomBytes[i] % allowedCharCount];
  17. return new string(chars);
  18. }

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

  1. namespace myWebApp
  2. {
  3. public partial class _Default : System.Web.UI.Page
  4. {
  5. protected void Page_Load(object sender, EventArgs e)
  6. {
  7. NameValueCollection myparas = new NameValueCollection();
  8. myparas.Add("idUser", "18759");
  9. myparas.Add("context", "reinit");
  10. myparas.Add("token", Securite.getUniqueKey(null));
  11. string myparams = Securite.createQSSecure(myparas);
  12. // suite des traitements, envoi d'une URL avec les params myparams
  13. }
  14. }
  15. }

Page qui réceptionne les paramètres précédemment fixés et les vérifie

  1. namespace myWebApp
  2. {
  3. public partial class pwdReinit : System.Web.UI.Page
  4. {
  5. protected void Page_Load(object sender, EventArgs e)
  6. {
  7. if (!Securite.isUrlSecureValid(Request.QueryString))
  8. throw new ArgumentException("Paramètres invalides...");
  9. // suite des traitements avec les paramètres passés
  10. }
  11. }
  12. }

Attaché (Annexes) au billet, les sources du cas pratique.