User Saved Safe URL without security risks

Often there is a need to have a url like:

https://myhost/AccountStatement.aspx?accountId=0392342&Date=20111109

Unfortunately this opens a security risk. The https protects the page contents but not the URL. The URL is sent as open text. The above URL tempts hackers that may also be users of the system to try enumerating various accountId in the hope that someone has forgotten to check the access permission against the user for each account (A naive assumption is that you don’t know the account number unless you are the owner…).

 

One solution is to put put the data in a post back and thus take it off the URL line – unfortunately it is not friendly because it prevents a user from saving the URL for quick reference.  You can have both friendly and secure by encrypting the arguments.

 

If you use a Membership Provider you have a key that you can use for encrypting, using the user name or host name results in a second key so you can now well encrypt the URL Parameters. I usually just concatenated the URL parameters with tabs between each ordered part to make decomposition using a split easy, i.e. 0392342\t20111109 is what I would encrypt and assign to ?key=

 

The first part is a SALT for which I use the MembershipUser ProviderID, typically a GUID.

Code Snippet
  1. private static byte[] UserSpecificKey
  2. {
  3.     get
  4.     {
  5.         var user = Membership.GetUser();
  6.         if (user == null)
  7.         {
  8.             return Encoding.ASCII.GetBytes("GUEST");
  9.         }
  10.         return Encoding.ASCII.GetBytes(user.ProviderUserKey.ToString());
  11.     }
  12. }

Then I use a standard built in two way encrypt/decrypt and use some other bit of information, for example, the host name (which means that the URL is host specific).

Code Snippet
  1. public static string EncryptStringAES(string plainText)
  2.         {
  3.             if (string.IsNullOrEmpty(plainText))
  4.                 throw new ArgumentNullException("plainText");
  5.             var hostName = HttpContext.Current.Request.Url.DnsSafeHost;
  6.  
  7.             string outStr = null;
  8.             RijndaelManaged aesAlg = null;
  9.  
  10.             try
  11.             {
  12.                 var key = new Rfc2898DeriveBytes(hostName, UserSpecificKey);
  13.                 aesAlg = new RijndaelManaged();
  14.                 aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
  15.                 aesAlg.IV = key.GetBytes(aesAlg.BlockSize / 8);
  16.                 var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
  17.  
  18.                 using (var msEncrypt = new MemoryStream())
  19.                 {
  20.                     using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
  21.                     {
  22.                         using (var swEncrypt = new StreamWriter(csEncrypt))
  23.                         {
  24.                             swEncrypt.Write(plainText);
  25.                         }
  26.                     }
  27.                     outStr = Convert.ToBase64String(msEncrypt.ToArray());
  28.                 }
  29.             }
  30.             finally
  31.             {
  32.                 if (aesAlg != null)
  33.                     aesAlg.Clear();
  34.             }
  35.             return outStr;
  36.         }

and

Code Snippet
  1. public static string DecryptStringAES(string cipherText)
  2. {
  3.     if (string.IsNullOrEmpty(cipherText))
  4.         throw new ArgumentNullException("cipherText");
  5.     var hostName = HttpContext.Current.Request.Url.DnsSafeHost;
  6.     RijndaelManaged aesAlg = null;
  7.     string plaintext = null;
  8.  
  9.     try
  10.     {
  11.         var key = new Rfc2898DeriveBytes(hostName, UserSpecificKey);
  12.         aesAlg = new RijndaelManaged();
  13.         aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
  14.         aesAlg.IV = key.GetBytes(aesAlg.BlockSize / 8);
  15.  
  16.         var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
  17.         byte[] bytes = Convert.FromBase64String(cipherText);
  18.         using (var msDecrypt = new MemoryStream(bytes))
  19.         {
  20.             using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
  21.             {
  22.                 using (var srDecrypt = new StreamReader(csDecrypt))
  23.                     plaintext = srDecrypt.ReadToEnd();
  24.             }
  25.         }
  26.     }
  27.     finally
  28.     {
  29.         if (aesAlg != null)
  30.             aesAlg.Clear();
  31.     }
  32.     return plaintext;
  33. }

 

The user can now save the URLs,  they will be directed to the login page (if not logged in) and then redirected to the page they saved. There is no ability for anyone to go fishing. If someone on the same computer finds the saved link, they will be asked for a login (they may be a valid user of the system) but the page will NOT grant them access to the data since it will fail to decode the parameters.

 

Someone searching the browser history will not find any information that is exploitable. A room mate with your wallet can’t call up and say “Hi, my name is Jack Smith, Account No 0392342 and I want to do a wire transfer, my SSN is…. “ The account number is not available in the browser history.

Comments

Popular posts from this blog

Yet once more into the breech (of altered programming logic)

Simple WP7 Mango App for Background Tasks, Toast, and Tiles: Code Explanation

How to convert SVG data to a Png Image file Using InkScape