ASP.NET: Obfuscating the Querystring

by Peter A. Bromberg, Ph.D.

Peter Bromberg
"The greatest challenge to any thinker is stating the problem in a way that will allow a solution." -Bertrand Russell.

Recently I answered somebody's post on the MS C# newsgroup. This person was trying to "pass a connection" to another ASP.NET application from a redirect or a link and couldn't figure out how to do it. Of course, you wouldn't be able to pass the connection object itself (nor should you even try) but you could put the connection string to create it on the QueryString, grab it back out in the Page_Load event of the receiving page, and then simply re-create the same connection. That was the essence of my suggestion - put the connection string on the QueryString.


Shortly thereafter Steve Orr, whose work I respect, responded that putting a connection string on the querystring posed a substantial security risk, which is correct. So what I'm about to offer here is a nice, compact way to encapsulate the QueryString in ASP.NET as an object that makes it much easier to manipulate, along with some nice code that will perform ASCII-to-HEX scrambling on both the names -- and the values -- of everything on the querystring. Actually you probably could never get an unencrypted connection string on the querystring anyway, it contains illegal "=" characters, so you'd have no choice but to mangle it in some way. (Similarly, we cannot use Base64 encoding since a Base64 encoded string ends with one or two equals signs.)

This isn't "encryption" - it's really scrambling or "obfuscation" - which is typically all you need to keep your "stuff" away from prying eyes. If you truly need strong encryption, my suggestion is -- don't use the querystring at all; find another way. Of course, you can add to this, for example doing TRIPLE-DES encryption before performing the ASCI-To-HEX, and the reverse for decrypting, if you wanted to do "overkill". But the bottom line is, it's best not to put sensitive data on the querystring at all -- there are other ways to pass the data.

First, I make use of Bobby DeRosa's QueryString class which is shown here virtually unchanged from his original post:

using System.Collections.Specialized ;

using System.Collections;

using System.Web;

using System;

namespace PAB.Utils

{

   // from Bobby DeRosa :

   // http://www.csharper.net/blog/

   // querystring_class_useful_for_querystring_manipulation__appendage__etc.aspx

 

    public class QueryString : NameValueCollection

    {

        private string document;

        public string Document

        {

            get

            {

                return document;

            }

        }

 

        public QueryString()

        {

        }

 

        public QueryString(NameValueCollection clone) : base(clone)

        {

        }

 

        public static QueryString FromCurrent()

        {

            return FromUrl(HttpContext.Current.Request.Url.AbsoluteUri);   

        }

 

        public static QueryString FromUrl(string url)

        {

            string[] parts = url.Split("?".ToCharArray());

            QueryString qs = new QueryString();

            qs.document = parts[0];

 

            if(parts.Length == 1)

                return qs;

 

            string[] keys = parts[1].Split("&".ToCharArray());

            foreach(string key in keys)

            {

                string[] part = key.Split("=".ToCharArray());

                if(part.Length == 1)

                    qs.Add(part[0], "");

                qs.Add(part[0], part[1]);

            }

 

            return qs;

        }

 

        public void ClearAllExcept(string except)

        {

            ClearAllExcept(new string[] { except });

        }

 

        public void ClearAllExcept(string[] except)

        {

            ArrayList toRemove = new ArrayList();

            foreach(string s in this.AllKeys)

            {

                foreach(string e in except)

                {

                    if(s.ToLower() == e.ToLower())

                        if(!toRemove.Contains(s))

                            toRemove.Add(s);

                }

            }

 

            foreach(string s in toRemove)

                this.Remove(s);

        }

 

        public override void Add(string name, string value)

        {

            if(this[name] != null)

                this[name] = value;

            else

                base.Add(name, value);

        }

 

        public override string ToString()

        {

            return ToString(false);

        }

 

        public string ToString(bool includeUrl)

        {

            string[] parts = new string[this.Count];

            string[] keys = this.AllKeys;

            for(int i = 0; i < keys.Length; i++)

                parts[i] = keys[i] + "=" + HttpContext.Current.Server.UrlEncode(this[keys[i]]);

            string url = String.Join("&", parts);

            if((url!=null || url!=String.Empty) && !url.StartsWith("?"))

                url = "?" + url;

            if(includeUrl)

                url = this.document + url;     

            return url;

        }

    }

}

What the above gives us is an easy way to work with the Querystring as a true "object" rather than having to write what I like to refer to as "BS code" just to get items and so on. For example, with a real object encapsulating the Querystring, we can write code like this:

private void btnGo_Click(object sender, System.EventArgs e)

        {

            QueryString  qs = new QueryString();

            qs.Add(this.txtName1.Text,this.txtValue1.Text);

            qs.Add(this.txtName2.Text,this.txtValue2.Text);

            qs.Add(this.txtName3.Text,this.txtValue3.Text);

            string strRedirect= "WebForm2.aspx";

            QueryString qsEncrypted = PAB.Utils.Encryption.EncryptQueryString(qs);

            strRedirect+=qsEncrypted.ToString();

            Response.Redirect(strRedirect,true);       

        }

Tell me that isn't a lot prettier and easier to work with! You can see the call the encryption utility passes an instance of the entire Querystring object, and receives back a new one, encrypted - both names and values. All we need to do is call the ToString() method and add it to the base redirect URL. Now, on the receiving end:

private void Page_Load(object sender, System.EventArgs e)

        {

            QueryString qs = QueryString.FromCurrent();           

 txtRaw.Text+=((QueryString)PAB.Utils.Encryption.DecryptQueryString(qs)).ToString();

        }

Finally, here's the Encryption Utility class that works with this, with the Hex routines I finally settled on:

using System;

using System.IO;

using System.Xml;

using System.Text;

using System.Security.Cryptography;

using System.Globalization ;

using System.Web;

 

namespace PAB.Utils

{

    public class Encryption

    {   

 

        private Encryption()

        {

        }

 

        public static QueryString EncryptQueryString(QueryString queryString)

        {           

            QueryString newQueryString = new QueryString();

            string nm=String.Empty;

            string val=String.Empty;

            foreach(string name in queryString)

            {

                nm= name;

                val=queryString[name];               

   newQueryString.Add(PAB.Utils.Encryption.Hex(nm), PAB.Utils.Encryption.Hex(val));

            }

            return newQueryString;

        }

 

        public static QueryString DecryptQueryString(QueryString queryString)

        {       

            QueryString newQueryString = new QueryString();

            string nm;

            string val;

            foreach(string name in queryString)

            {

                nm=PAB.Utils.Encryption.DeHex(name);

                val=PAB.Utils.Encryption.DeHex(queryString[name]);               

                newQueryString.Add(nm, val );

            }

                return newQueryString;

        }

 

        public static string DeHex(string hexstring)

        {

            string ret = String.Empty;

            StringBuilder sb = new StringBuilder(hexstring.Length /2);

            for(int i=0;i<= hexstring.Length-1;i=i+2)

            {

 sb.Append((char)int.Parse(hexstring.Substring(i,2), NumberStyles.HexNumber));              }

            return sb.ToString();

        }

 

    public static string Hex(string sData)

        {

            string temp = String.Empty;;

            string newdata=String.Empty;

            StringBuilder sb = new StringBuilder(sData.Length *2);

            for (int i = 0;i < sData.Length;i++)

            {

                if ((sData.Length - (i + 1)) > 0)

                {

                    temp = sData.Substring(i,2);

                    if (temp == @"\n") newdata += "0A";

                    else if (temp == @"\b") newdata += "20";

                    else if (temp == @"\r") newdata += "0D";

                    else if (temp == @"\c") newdata += "2C";

                    else if (temp == @"\\") newdata += "5C";

                    else if (temp == @"\0") newdata += "00";

                    else if (temp == @"\t") newdata += "07";

                    else

                    {

    sb.Append(  String.Format("{0:X2}", (int)(sData.ToCharArray())[i]));

                        i--;

                    }

                }

                else

                {

   sb.Append(  String.Format("{0:X2}", (int)(sData.ToCharArray())[i]));

                }

                i++;

            }

            return sb.ToString();

        }

    }

}

I experimented with XOR, ROT13, ROT39 and ASCII-to-Hex, which is the method I finally settled on. Since this method doesn't require any UrlEncoding normalization, that part of the QueryString class has been commented out.

The net result of all this is that this QueryString (which would not even pass as it contains illegal characters):

WebForm2.aspx?connstring=server=xyz;database=yoda;uid=jocko;pwd=youbetcha;

would now look like so:

WebForm2.aspx?636F6E6E737472696E67=7365727665723D78797A3B646174616261
73653D796F64613B7569643D6A6F636B6F3B7077643D796F756265746368613B

You can see the two static methods EncryptQuerystring and DecryptQuerystring which accept and return Querystring objects. So now, if you have a need to scramble your QueryStrings for "basic security", this becomes an easy way to handle it. A nice side benefit of this is that now you can put whatever you want into a querystring item - spaces, semicolons, slashes, equals signs -- you name it, and it will be faithfully reproduced on the receiving end.

N.B. Thanks to Steve and Richard for their coding suggestions, both of which have been incorporated into the downloadable source code below. To others, who seem to take delight in bashing people without even having read the third paragraph of this article, I've left all your comments there so readers can decide for themselves whether this concept is useful. Once again, I will repeat what I said quite clearly in the third paragraph above, that some people seem to have conveniently ignored since they are so anxious to be armchair quarterbacks:

If you truly want secure transmission of items from one page to another, DO NOT put them on the querystring.

Download the Visual Studio 2003 Solution accompanying this article


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform. Pete's samples at GotDotNet.com have been downloaded over 41,000 times. You can read Peter's UnBlog Here.  --><--NOTE: Post QUESTIONS on FORUMS!
Article Discussion: