Skip to main content

You must have seen websites that have URLs with some long and -- at first sight -- meaningless query string parameters. The goal is often to construct tamperproof query strings to protect the integrity of the parameters such as a customer id. The goal is make sure that this parameter has not been modified on the client side. Note that the actual ID will still be visible in the query string, but we extend this URL to include a special hash. If the client modifies either the 'cid' or the 'h' parameter then the request will be rejected.

/// <summary>
/// Tamper-proof Query Strings
/// https://github.com/andras-nemes/cryptographydotnet/blob/master/TamperProofQueryStrings/HashingHelper.cs
///
/// Hashing algorithms and their practical usage in .NET Part 1
/// http://dotnetcodr.com/2013/10/28/hashing-algorithms-and-their-practical-usage-in-net-part-1/
///
/// You must have seen websites that have URLs with some long and -- at first
/// sight -- meaningless query string parameters. The goal is often to
/// construct tamperproof query strings to protect the integrity of the
/// parameters such as a customer id. The goal is make sure that this parameter
/// has not been modified on the client side. Note that the actual ID will
/// still be visible in the query string, e.g. /Customers.aspx?cid=21 but we
/// extend this URL to include a special hash: /Customers.aspx?cid=21&amp;hash=sdfhshmfuismfsdhmf.
/// If the client modifies either the 'cid' or the 'h' parameter then the
/// request will be rejected.
/// </summary>
public class HashingHelper
{
    /// <summary>
    /// The hash query separator stores the hash param identifier in the
    /// query string.
    /// </summary>
    public static readonly string _hashQuerySeparator = "&h=";

    /// <summary>
    /// The key will be used to stop an attacker from creating an own hash
    /// as mentioned above.
    /// </summary>
    public static readonly string _hashKey = "C2CE6ACD";

    /// <summary>
    /// Creates the hashed query.
    /// </summary>
    /// <param name="basicQueryString"></param>
    public static string CreateTamperProofQueryString(string basicQueryString)
    {
        return string.Concat(basicQueryString, _hashQuerySeparator, ComputeHash(basicQueryString));
    }

    /// <summary>
    /// Computes the the hashed value.
    /// </summary>
    /// <param name="basicQueryString"></param>
    private static string ComputeHash(string basicQueryString)
    {
        // add the unique session key to the data that's being hashed:
        HttpSessionState httpSession = HttpContext.Current.Session;
        basicQueryString += httpSession.SessionID;
        httpSession["HashIndex"] = 10;

        byte[] textBytes = Encoding.UTF8.GetBytes(basicQueryString);
        HMACSHA1 hashAlgorithm = new HMACSHA1(Conversions.HexToByteArray(_hashKey));
        byte[] hash = hashAlgorithm.ComputeHash(textBytes);

        return Conversions.ByteArrayToHex(hash);
    }
}

/// <summary>
/// Conversions static utility class.
/// </summary>
public static class Conversions
{
    public static byte[] HexToByteArray(string hexString)
    {
        if (0 != (hexString.Length % 2))
        {
            throw new ApplicationException("Hex string must be multiple of 2 in length");
        }

        int byteCount = hexString.Length / 2;
        byte[] byteValues = new byte[byteCount];
        for (int i = 0; i < byteCount; i++)
        {
            byteValues[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
        }

        return byteValues;
    }

    public static string ByteArrayToHex(byte[] data)
    {
        return BitConverter.ToString(data);
    }
}

//
// USAGE
// ============================================================================

//
// In the Default.aspx.cs file add the following code:
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        lnkAbout.NavigateUrl = string.Concat("/About.aspx?", HashingHelper.CreateTamperProofQueryString("cid=21&pid=43"));
    }
}

//
// Run the project and hover over the generated link. You should see in the bottom
// of the web browser that the cid and pid parameters have been extended with the
// extra 'h' hash parameter.
//
// Add a label control somewhere on the About page:
// <asp:Label ID="lblQueryValue" runat="server" Text=""></asp:Label>

//
// Put the following in the code behind:
protected override void OnLoad(EventArgs e)
{
    HashingHelper.ValidateQueryString();
    base.OnLoad(e);
}

//
// where ValidateQueryString looks as follows:
public static void ValidateQueryString()
{
    HttpRequest request = HttpContext.Current.Request;

    if (request.QueryString.Count == 0)
    {
        return;
    }

    string queryString = request.Url.Query.TrimStart(new char[] { '?' });

    string submittedHash = request.QueryString[HashingHelper._hashQuerySeparator];
    if (submittedHash == null)
    {
        throw new ApplicationException("Querystring validation hash missing!");
    }

    int hashPos = queryString.IndexOf(_hashQuerySeparator);
    queryString = queryString.Substring(0, hashPos);

    if (submittedHash != ComputeHash(queryString))
    {
        throw new ApplicationException("Querystring hash value mismatch");
    }
}