Click here to Skip to main content
15,867,834 members
Articles / Web Development / ASP.NET
Article

Scramble Your Query Strings

Rate me:
Please Sign up or sign in to vote.
4.09/5 (18 votes)
5 Oct 20055 min read 174.3K   712   73   29
Protect your query strings from inquisitive users.

Image 1

Introduction

Query strings are a very convenient way of passing information to a new window or to a following page. The problem with query strings is that the information is visible. In my experience, the wiseass users see the query strings in the browser and try to duplicate the functionality: like "Oh, when I click the button, I get 'DeleteInventory.aspx?item=123&user=george&id=99' so, if I want to delete the item #222, I just need to use 'DeleteInventory.aspx?item=222&user=george&id=99' right?" Wrong, the id=99 is the database ID for the item to be removed. The rest of the information is for populating the label controls on the page. To avoid this problem, either use POST instead of GET (made much easier in ASP.NET 2.0) or scramble the information in the querystring. Nobody will make a mess with 'DeleteInventory.aspx?value=lkajhskjhsdfoiuhrt8974325lkjh'.

Solving the visible query string problem

Scrambling the query string allows you to pass information securely. The information displayed after the page URL is simply a jumble of characters. The query string information is scrambled prior to invoking the called page and is descrambled by the newly opened page. The scrambling and descrambling mechanism hides the information from the users while allowing you to continue using the convenient query string mechanism. The keys used to scramble and descramble the query strings are session-specific, allowing all the windows in a session to share information.

Why keep keys in session?

Many applications use a named window to perform a specific function. A user might have multiple browser windows referencing the same session (usually by using CTRL-N (Internet Explorer) to open new browser windows). All the windows in the session should scramble query strings the same way (using the same keys and initialization vectors) to allow the destination window to correctly decode the query string.

Scrambling and descrambling

The approach described here uses RC2 encryption to scramble (encrypt) and descramble (decrypt) the query string. Other algorithms such as ROT13 (simple replacement) DES and AES could be used as well. RC2 was chosen because it is relatively fast (efficiently implemented) and is well documented. Note that the purpose here is just to scramble (and later descramble) the query string. If you need to protect your information from malicious hackers, you should:

  • not be using RC2,
  • not put sensitive information in query strings.

Tamper-proofing your query strings

A secondary issue here is making sure the user cannot randomly modify the scrambled query string in a way that results in a valid, different, query string. Using a block cipher like RC2 solves this problem as the chance of a modified scrambled string passing decryption is very low. An Alternative approach is to embed a checksum (such as a CRC) in the string, when decoding the scrambled string verify the checksum to make sure the string was not modified.

Deriving from System.Web.Page

A simple approach to extend the web page functionality is to extend the System.Web.Page class. Instead of having your page inherit directly from System.Web.Page, inherit from a class (e.g. MyPage) derived from System.Web.Page. The added functionality is then made available to all web pages.

Handling keys and initialization vectors

The MyPage class defines the following methods and properties to handle algorithm keys and initialization vectors. (RC2 is a block cipher and requires both a key and an initialization vector to encode and decode information.)

C#
// Key management for scrambling support
public byte[] ScrambleKey
{
    set 
    {
        byte[] key = value;
        if (null == key) 
        {
            // Use existing key if non provided
            key = ScrambleKey;
        }
        Session["ScrambleKey"] = key;
    }
    get 
    {
        byte[] key = (byte[])Session["ScrambleKey"];
        if (null == key)
        {
            RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();
            rc2.GenerateKey();
            key = rc2.Key;
            Session["ScrambleKey"] = key;
        }
        return key;
    }
}

// Initialization vector management for scrambling support
public byte[] ScrambleIV
{
    set 
    {
        byte[] key = value;
        if (null == key) 
        {
            key = ScrambleIV;
        }
        Session["ScrambleIV"] = key;
    }
    get 
    {
        byte[] key = (byte[])Session["ScrambleIV"];
        if (null == key)
        {
            RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();
            rc2.GenerateIV();
            key = rc2.IV;
            Session["ScrambleIV"] = key;
        }
        return key;
    }
}

As you can see above, the key and the initialization vector are generated on first access and stored in the session. I would not recommend this as a method for secure communication, but it is an acceptable solution for simple scrambling.

Scrambling and descrambling the query string

The Scramble(string message) method takes a string and returns a Base64 encoded scrambled string. Note that the original string length is prepended to the string – RC2 is a block cipher, without the length indicator you may get some padding at the end of the string after descrambling:

C#
/// <summary>
/// Scramble a message using a session-specific key
/// </summary>
public string Scramble(string message)
{
    UTF8Encoding textConverter = new UTF8Encoding();
    RC2CryptoServiceProvider rc2CSP = new RC2CryptoServiceProvider();

    //Convert the data to a byte array.
    byte[] toEncrypt = textConverter.GetBytes(message);

    //Get an encryptor.
    ICryptoTransform encryptor = 
                     rc2CSP.CreateEncryptor(ScrambleKey, ScrambleIV);
            
    //Encrypt the data.
    MemoryStream msEncrypt = new MemoryStream();
    CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor,
                                             CryptoStreamMode.Write);

    //Write all data to the crypto stream and flush it.
    // Encode length as first 4 bytes
    byte[] length = new byte[4];
    length[0] = (byte)(message.Length & 0xFF);
    length[1] = (byte)((message.Length >> 8) & 0xFF);
    length[2] = (byte)((message.Length >> 16) & 0xFF);
    length[3] = (byte)((message.Length >> 24) & 0xFF);
    csEncrypt.Write(length, 0, 4);
    csEncrypt.Write(toEncrypt, 0, toEncrypt.Length);
    csEncrypt.FlushFinalBlock();

    //Get encrypted array of bytes.
    byte[] encrypted = msEncrypt.ToArray();

    // Convert to Base64 string
    string b64 = Convert.ToBase64String(encrypted);

    // Protect against URLEncode/Decode problem
    string b64mod = b64.Replace('+', '@');

    // Return a URL encoded string
    return HttpUtility.UrlEncode(b64mod);

The Descramble(string scrambledMessage) method takes a scrambled string (one produced by Scramble()) and returns the original, un-scrambled string:

C#
/// <summary>
/// Descramble a message in a session-specific key
/// </summary>
public string Descramble(string scrambledMessage)
{
    UTF8Encoding textConverter = new UTF8Encoding();
    RC2CryptoServiceProvider rc2CSP = new RC2CryptoServiceProvider();

    // URL decode , replace and convert from Base64
    string b64mod = HttpUtility.UrlDecode(scrambledMessage);
    // Replace '@' back to '+' (avoid URLDecode problem)
    string b64 = b64mod.Replace('@', '+');
    // Base64 decode
    byte[] encrypted = Convert.FromBase64String(b64);

    //Get a decryptor that uses the same key and IV as the encryptor.
    ICryptoTransform decryptor = 
                      rc2CSP.CreateDecryptor(ScrambleKey, ScrambleIV);

    //Now decrypt the previously encrypted message using the decryptor
    // obtained in the above step.
    MemoryStream msDecrypt = new MemoryStream(encrypted);
    CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor,
        CryptoStreamMode.Read);

    byte[] fromEncrypt = new byte[encrypted.Length-4];

    //Read the data out of the crypto stream.
    byte[] length = new byte[4];
    csDecrypt.Read(length, 0, 4);
    csDecrypt.Read(fromEncrypt, 0, fromEncrypt.Length);
    int len = (int)length[0] | (length[1] << 8)
        | (length[2] << 16) | (length[3] << 24);

    //Convert the byte array back into a string.
    return textConverter.GetString(fromEncrypt).Substring(0, len);
}

The helper method, DescrambleQueryString(string scrambledMessage) uses Descramble() to decode the query string and returns a NameValueCollection, allowing easy access to the query string values. Simply put, a NameValueCollection is a Hashtable with both the key and the value typed as strings. The NameValueCollection can be found in System.Collections.Specialized:

C#
/// <summary>
/// Descramble the query string and return a 
/// NameValueCollection containing key and value
/// pairs corresponding to the original query string.
/// </summary>
public NameValueCollection DescrambleQueryString(string scrambledMessage)
{
    // Decode the query string
    string queryString = Descramble(scrambledMessage);
    NameValueCollection result = new NameValueCollection();
    char[] splitChar = new char[] {'&'};
    char[] equalChar = new char[] {'='};
    // Split query string to components
    foreach (string s in queryString.Split(splitChar)) 
    {
        // split each component to key and value
        string[] keyVal = s.Split(equalChar, 2);
        string key = keyVal[0];
        string val = String.Empty;
        if (keyVal.Length > 1) val = keyVal[1];
        // Add to the hashtable
        result.Add(key, val);
    }

    // return the resulting hashtable
    return result;
}

Example: scrambling and descrambling a query string

To prepare and scramble a query string:

C#
string qs = "masterFlag=Y&chartDepth=0&masterAcctID=" + masterAcctID
              + "&clientName=" +pagerEntry.ClientName  
              + "&vendorName=" + pagerEntry.VendorName
              +"&accountID=0&masterAccount=" + pagerEntry.MasterAccountNumber;
string scrambled_qs = PLSession.Scramble(qs);
objSubDataRow["hypDeleteAccount"] = 
   "javascript:openwincust('/ AccountDelete.aspx?query=" + scrambled_qs + "');";

To descramble a query string:

C#
using System.Collections.Specialized;

// Read de-scrambled query string parameters
NameValueCollection queryString = 
              DescrambleQueryString(Request.QueryString["query"]);
lblClientName.Text    = queryString["clientName"];
lblAccountID.Text    = queryString["accountID"];
lblMasterAccount.Text = queryString["masterAccount"];
lblVendorName.Text    = queryString["vendorName"];
m_chartDepth        = Convert.ToInt32(queryString["chartDepth"]);
m_masterFlag        = queryString["masterFlag"];
m_masterAccountID    = Convert.ToInt32(queryString["masterAcctID"]);

The URLDecode() problem

The URLDecode() method of System.Web.HttpUtility does not behave in an intuitive fashion when called repeatedly. For example: calling UrlEncode() with the string "abc+=" results in "abc%2b%3d". The first call to URLDecode() with "abc%2b%3d" correctly results in "abc+=". Calling UrlDecode() again with "abc+=" results in "abc =" - the '+' was replaced with a space. Why is this a problem at all? This is a problem because some .NET Framework methods silently call URLDecode(). My testing shows that the results from Response.QueryString[] is URL-decoded even though nothing is mentioned in the documentation. To avoid this problem, I recommend replacing all the '+' characters with '@' in the Base64 output string. The '@' character is URL safe and does not change when multiple calls are made to UrlDecode().

Author notes

If you can, always hide the information passed between pages. Use POST, keep things in Session or use Context. When dealing with legacy code where everything is passed as query strings, I recommend scrambling the information to protect your application from being misused.

Further work

The scrambling algorithm can be broken into a separate class and an interface can be defined allowing multiple algorithms to be used. I believe such an action would be overkill. All that the code is trying to do is to prevent a nosy user from modifying the query strings.

History

  • 5/05 - Version 0.9
    • Used a Hashtable in the DescrambleQueryString() method.
  • 9/05 - Version 1.0
    • Converted Hashtable to NameValueCollection.
  • 10/05 - Version 1.1
    • Added URL encoding and decoding to protect Base64 string.
  • 10/05 - Version 1.2
    • Added protection against multiple calls to URLDecode().

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
Giora Tamir has been Architecting, Designing and Developing software and hardware solutions for over 15 years. As an IEEE Senior member and a talented developer, Giora blends software development, knowledge of protocols, extensive understanding of hardware and profound knowledge of both Unix and Windows based systems to provide a complete solution for both defense and commercial applications. Giora, also known as G.T., now holds the position of Principal Engineer for ProfitLine, Inc. architecting the next generation of .NET applications based on a Service-Oriented-Architecture.

Gioras areas of interest include distributed applications, networking and cryptography in addition to Unix internals and embedded programming.

Founded in 1992, ProfitLine manages hundreds of millions of dollars in annual telecom spend for its prestigious Fortune 1000 client base, such as Merrill Lynch, Charming Shoppes, Macromedia, CNA Financial Corporation, and Constellation Energy Group. ProfitLine's outsourced solution streamlines telecom administrative functions by combining a best practices approach with intelligent technology. For more information about ProfitLine, call 858.452.6800 or e-mail <a href=mailto:sales@profitline.com>sales@profitline.com.

Comments and Discussions

 
GeneralPlease reformat your article Pin
fwsouthern28-Sep-05 17:18
fwsouthern28-Sep-05 17:18 
GeneralRe: Please reformat your article Pin
gtamir29-Sep-05 8:13
gtamir29-Sep-05 8:13 
GeneralRe: Please reformat your article Pin
Martinac30-Sep-05 10:33
Martinac30-Sep-05 10:33 
GeneralRe: Please reformat your article Pin
gtamir30-Sep-05 11:19
gtamir30-Sep-05 11:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.