To read the article online, visit http://www.4GuysFromRolla.com/articles/083105-1.aspx

Passing Tamper-Proof QueryString Parameters

By Scott Mitchell


Article Notes
Aug. 31st, 2005: A related article has been written, Creating Expiring Web Pages.
 
Sept. 6th, 2005: This article's title was changed from Creating Tamper-Proof URLs to Passing Tamper-Proof QueryString Parameters.
 
Sept. 5th, 2005: This article's impetus was based on a real-world application I worked on recently that involved the need for one site to be able to send users to a partner site, sending along authentication information. Specifically, the first site (Site A) needed to be able to generate a URL that its users would click to take them to a partner site (Site B). Site A and Site B, then, worked together to define precisely what set of tamper-proof parameters would need to be passed and in what order.

The approach discussed in this article is a slightly more generalized and simplified look at the solution used in my project. This approach is less than ideal if you're wanting to create an entire site where all links are tamper-proof. For example, the code in this article is sensitive to the ordering and the set of tamper-proof querystring parameters; ergo, both the sending and receiving page must agree upon these tamper-proof parameters and their ordering, which can be prohibitive. An upcoming article will discuss are more generalized approach for creating truly tamper-proof URLs, ones that do not require the receiving and sending page to agree upon the set of tamper-proof querystring parameters.

Introduction


One of the unique challenges of building Web applications lies in way by which a Web application can be "invoked" by an end user. With traditional desktop applications, there is typically a very stringent set of ways the application can be invoked - by running a particular executable, perhaps passing in a set of command-line parameters. With Web applications, however, there are, possible, an infinite number of ways that the application can be invoked. With a Web application, each Web page serves as a public interface to the Web applications, and for Web pages whose functionality is based on user-supplied parameters (i.e., querystring or form-posted values) each potential input represents a unique interface.

Having a potentially unlimited number of public interfaces greatly increases the complexity and forethought required in building secure and consistent Web applications. Since URLs can easily be changed by even the most novice user, it is paramount that you do not place any state information in the querystring that you do not mind the user change, or, if you do, you need to validate in the web page's code to ensure that the user has not modified the querystring to an unacceptable state. For example, imagine that you had a website with a page where a user could modify their account. If you simply identified the logged on user by a UserID value in the querystring, like EditProfile.aspx?UserID=UserID, a savvy user would notice this and could modify other users' data by simply tweaking the querystring parameter.

However, there are times where important state needs to be passed through the querystring and, under no circumstances, should be it be able to be modified by the end user. (We'll discuss some of these cases in more detail further on in this article.) Such tamper-proof URLs can be created quite easily by using a one-way hash to sign the querystring parameters that you do not want edited and appending that signature to the querystring. The web page being visited, then, can apply the same hash to the plain-text querystring parameters and ensure that it matches up to the signature included in the querystring. If the two signatures match, then the querystring parameters have not been modified.

In this article we'll look at how to use one-way hashes to create tamper-proof URLs. As we'll see, only a few short lines of code are needed to both generate the signature on the page generating the hyperlink and validating the signature on the "receiving" page. Read on to learn more!

Uses for Tamper-Proof URLs


When creating web pages that accept user-defined inputs, be it through the querystring or a form, you should always do validation on the provided values. For example, if you have a site that has a page called ShowProduct.aspx?ID=ProductID, and only certain users can see certain products, it's imperative that in the web page's source code you always verify that the current user has permission to view the requested product. Even if you only create links on your site that have the valid ProductID value in the querystring, a user could easily modify the querystring parameters in an attempt to view products they don't have rights to view.

In cases where you can do validation of input parameters in the source code, you don't need to worry about creating tamper-proof URLs. After all, if the end user does tamper with the URL, you'll catch it and can respond accordingly. However, there are real-world scenarios where you may not be able to perform validation of the input parameters. Returning to our ShowProducts.aspx?ID=ProductID example, if only certain users can see certain products it's easy to check that the current user has permission to view the requested product... assuming that the only people that can visit ShowProducts.aspx are logged in users. But what if you want to be able to give users without an account on your site a "sneak peak" into various products, and you didn't want to just limit them to a pre-defined subset of products, but be able to give a sneak peak to any product at any time? Here we have a sort of chicken and egg problem: if we make allow anyone access to visit ShowProducts.aspx then we can't check to see if the current user has access to view product ProductID since the visitor might not have an account; if, however, we limit access to ShowProducts.aspx to only authenticated users, then these non-authenticated users can't try out the system. To solve this problem we can allow anyone in to ShowProducts.aspx, but ensure that users can only view the page they were granted through the use of a tamper-proof URL.

Another use for tamper-proof URLs is to limit the duration a particular resource can be accessed. For example, imagine that you have a website that you charge customers a monthly fee to use. In order to woo a potential customer, you might want to give them a chance to visit and check out the site. Rather than having to go through the hassle of creating a new user account that you'll have to delete if the user decides not to subscribe to the site, you can create a page that will automatically log in the visitor as a guest user. This auto-login URL would include a required querystring parameter called GoodUntil, which would be the date through which the potential customer's link is active. The auto-login URL, then, would check to ensure that the visitor's GoodUntil date was valid (i.e., greater than or equal to the current date). You could use a tamper-proof URL to ensure that this potential client didn't artificially extend his GoodUntil date.

There are other scenarios where tamper-proof URLs come in handy. If you have any you'd like to share to be included in this article, please let me know. What's important to understand is that creating a tamper-proof URL does not hide the querystring parameters from the end user. The user will still see the querystring parameters as plain-text in their Address bar. What tamper-proof URLs do afford, though, is the assurance that the user can't change the values of the signed querystring parameters.

Reader Comments
"How about using tamper-proof Urls for passing authentication across sites. For example we have 2 software products with separate sites that share a support site (all with different 2nd level domains) If a user was logged in to a product site and wanted to submit a support request to the support site I could forward them to the support site with [the username in a tamper-proof URL]."
-- Andy A.

Scott: Yes, this would work. A couple of issues/concerns, though: both websites would have to sync up on the secret salt being used, which could be a potential security weakness of the system. Additionally, keep in mind that the querystring parameters are being passed over the Internet in plain-text, so you'd only want to pass along non-sensitive information (like the person's username), or else use encryption techniques. Finally, to help prevent against replay attack you'd probably want to consider adding a tamper-proof time field to the URL and have the receiving server/page only accept requests within a certain timeframe (say one minute).

Protecting Against Tampering Using Hashes


In order to make a URL tamper-proof we're going to take those querystring parameters that cannot be modified and create a digest of them by using a one-way hash function (MD5, in this example). A one-way hash function takes as input a message and creates a shortened digest of the message. Hashes are said to be one-way because given the digest and the hashing algorithm, you cannot determine the original message value. This hash digest will be sent along with the regular querystring parameters. The receiving web page can then take the querystring parameters received, apply the same hashing function, and check to see if its version of the digest matches up with the digest sent through the querystring. If so, then we know that the querystring contents have not been modified en route; if there is a discrepancy, the querystring values received differ from those used to generate the digest.

To help concretize things, let's look at a simple (although incomplete/incorrect) example. Imagine that we want to create the following tamper-proof URL: ShowProduct.aspx?ID=5. The first thing we need to do is create a digest of those querystring parameters that we want to be tamper-proof. In this case, we have just one, ID=5. So we hash ID=5. Using MD5, a common hashing algorithm, the digest of "ID=5" is 53e5e07397f7f01c2b276af813901c29. That means that the URL we'd present to the end user would not be ShowProduct.aspx?ID=5, but ShowProduct.aspx?ID=5&Digest=53e5e07397f7f01c2b276af813901c29 instead. In ShowProduct.aspx we'd write code that would first ensure that there was a Digest value. Next, it would take the value of the ID querystring parameters (ID=5), hash it (getting 53e5e07397f7f01c2b276af813901c29), and compare it to the passed-in Digest value (53e5e07397f7f01c2b276af813901c29). If they match up (which they do, in this example), then we know that the querystring parameters haven't been tampered with. If they don't match up - that is, if our user attempts to change the ID from 5 to 6 - we'll need to display some error message (note that the hash of "ID=6" is 34399f012556aa724734de93378773bb, so we'll see that 53e5e07397f7f01c2b276af813901c29 and 34399f012556aa724734de93378773bb don't match up).

While this approach works great in preventing the user from tampering with just the ID querystring parameter, a more savvy end user might realize what we're up to and find a way around it. For example, if our savvy user wanted to pass in an ID value of 6 and realized that we were using MD5 to hash ID=6, he could compute the MD5 hash of "ID=6" and then modify both the ID and Digest values accordingly (i.e., visit ShowProduct.aspx?ID=6&Digest=34399f012556aa724734de93378773bb). The code in ShowProduct.aspx wouldn't complain, since the input parameter and hash value matched up, but clearly the URL has been tampered with!

What we need to do is salt the hash with some secret text. A salt is a string that is typically appended to the string to be hashed. For example, we may use a secret salt of "Scott"; with this change, when generating the URL for ShowProduct.aspx?ID=5 we'd create a digest for "ID=5Scott" as opposed to just "ID=5". Since the end user doesn't know our secret salt, they can't correctly form the digest for a desired ID value.

Actually, we'd be better off if we used the salt to both prepend and append the input string, instead getting a digest for ScottID=5Scott. There are many sites online that have databases full of MD5 digests and corresponding plain-text values. If you have an overly simple input to the MD5 algorithm, chances are one of these databases will have a mapping from the digest back to plain-text. This is why I'd encourage you to both prepend and append the salt to the plain-text password. Additionally, use a long salt with a wide mix of characters. You can even apply the MD5 algorithm multiple times, like MD5(MD5(salt+input+salt)). Remember, with security there is no guarantee; all you can do is make it harder for would-be hackers to compromise the system.

Generating a Tamper-Proof Link


There are two bits of code we need to examine: how to create a tamper-proof link and how to verify that the URL has not been tampered with. This section explores the first task, which will be used in web pages that need to generate a tamper-proof URL. The second task is examined in the next section, and is used in the requested web page to ensure that the passed-in values were not tinkered with.

In order to generate the digest to tack on to the querystring we need to have two bits of information:

  1. The secret salt, and
  2. The list of querystring parameters that should be tamper-proof
The digest, then, is computed by using the MD5 algorithm (which is provided as a class in the .NET Framework), with the input being the concatenation of the salt and the list of tamper-proof querystring parameters. To make this process easier, I encapsulated the necessary functionality into a couple of methods, as shown in the code snippet below:

Sub Page_Load(sender as Object, e as EventArgs)
  lnkToB.NavigateUrl = CreateTamperProofURL("TamperProofURLs.B.aspx", _
                         "NonTamperProof=1", "TP1=Scott&TP2=27&TP3=False")
End Sub

'The secret salt...
Private Const SecretSalt = "H3#@*ALMLLlk31q4l1ncL#@..."

Function CreateTamperProofURL(url as String, nonTamperProofParams as String, _
                              tamperProofParams as String) as String
  Dim tpURL as String = url
  If nonTamperProofParams.Length > 0 OrElse tamperProofParams.Length > 0 Then
    url &= "?"
  End If
  
  'Add on the tamper & non-tamper proof parameters, if any
  If nonTamperProofParams.Length > 0 then
    url &= nonTamperProofParams
    
    If tamperProofParams.Length > 0 Then url &= "&"
  End If
  
  If tamperProofParams.Length > 0 Then url &= tamperProofParams
  
  'Add on the tamper-proof digest, if needed
  If tamperProofParams.Length > 0 Then    
    url &= String.Concat("&Digest=", GetDigest(tamperProofParams)) 
  End If
  
  Return url
End Function


Function GetDigest(tamperProofParams as String) as String
  Dim Digest as String = String.Empty
  Dim input as String = String.Concat(SecretSalt, tamperProofParams, SecretSalt)
    
  'The array of bytes that will contain the encrypted value of input
  Dim hashedDataBytes As Byte()

  'The encoder class used to convert strPlainText to an array of bytes
  Dim encoder As New System.Text.UTF8Encoding

  'Create an instance of the MD5CryptoServiceProvider class
  Dim md5Hasher As New System.Security.Cryptography.MD5CryptoServiceProvider

  'Call ComputeHash, passing in the plain-text string as an array of bytes
  'The return value is the encrypted value, as an array of bytes
  hashedDataBytes = md5Hasher.ComputeHash(encoder.GetBytes(input))

  'Base-64 Encode the results and strip off ending '==', if it exists
  Digest = Convert.ToBase64String(hashedDataBytes).TrimEnd("=".ToCharArray())
  
  Return Digest
End Function
[View a Live Demo!]

The CreateTamperProofURL() method takes in three input parameters and returns the tamper-proof URL. The first input parameter is the name of the web page the link should point to; the second parameter is the list of querystring parameters that are not tamper-proof. That is, these are the querystring parameters that can be changed either by the user or through some client-side script or server-side code; the final input parameter are the list of querystring parameters that are intended to be tamper-proof.

The URL generated tacks on both the tamper proof and non-tamper proof querystring parameters, along with a digest of the tamper proof querystring parameters. This digest is computed by calling the GetDigest() method, which utilizes the System.Security.Cryptography.MD5CryptoServiceProvider class to compute the MD5 hash of the input. (Notice that the input is, specifically, the secret salt, concatenated with the tamper-proof querystring parameters, concatenated with the secret salt.)

In the Page_Load event handler the NavigateUrl property of a HyperLink control is set to the URL returned by CreateTamperProofURL(). In the next section we'll examine the code needed in the receiving web page to ensure that the querystring parameters have not been tampered with.

Ensuring That The QueryString Has Not Been Tampered With


The last step is to write the code in the receiving page that makes sure that the querystring parameters have not been tampered with. This is accomplished by taking the plain-text querystring parameters and computing their digest. Next, this computed digest is compared to the digest passed through the querystring. If the two match up we know that the tamper-proof data came through without having been modified. If these digests do not match up, something was changed, and we can take appropriate action.

Sub Page_Load(sender as Object, e as EventArgs)
  EnsureURLNotTampered(String.Format("TP1={0}&TP2={1}&TP3={2}", _
                            Request.QueryString("TP1"), _
                            Request.QueryString("TP2"), _
                            Request.QueryString("TP3")))
End Sub

'The secret salt...
Private Const SecretSalt = "H3#@*ALMLLlk31q4l1ncL#@RFH..."

Sub EnsureURLNotTampered(tamperProofParams as String)
  'Determine what the digest SHOULD be
  Dim expectedDigest as String = GetDigest(tamperProofParams)
  
  'Any + in the digest passed through the querystring would be
  'convereted into spaces, so 'uncovert' them
  Dim receivedDigest as String = Request.QueryString("Digest")
  If receivedDigest Is Nothing Then
    'Oh my, we didn't get a Digest!
    Response.Write("YOU MUST PASS IN A DIGEST!")
    Response.End()
  Else
    receivedDigest = receivedDigest.Replace(" ", "+")
    
    'Now, see if the received and expected digests match up
    If String.Compare(expectedDigest, receivedDigest) <> 0 Then
      'Don't match up, egad
      Response.Write("THE URL HAS BEEN TAMPERED WITH.")
      Response.End()
    End If
  End If
End Sub

Function GetDigest(tamperProofParams as String) as String
  Dim Digest as String = String.Empty
  Dim input as String = String.Concat(SecretSalt, _
                                        tamperProofParams, SecretSalt)
    
  'The array of bytes that will contain the encrypted value of input
  Dim hashedDataBytes As Byte()

  'The encoder class used to convert strPlainText to an array of bytes
  Dim encoder As New System.Text.UTF8Encoding

  'Create an instance of the MD5CryptoServiceProvider class
  Dim md5Hasher As New System.Security.Cryptography.MD5CryptoServiceProvider

  'Call ComputeHash, passing in the plain-text string as an array of bytes
  'The return value is the encrypted value, as an array of bytes
  hashedDataBytes = md5Hasher.ComputeHash(encoder.GetBytes(input))

  'Base-64 Encode the results and strip off ending '==', if it exists
  Digest = Convert.ToBase64String(hashedDataBytes).TrimEnd("=".ToCharArray())
  
  Return Digest
End Function
[View a Live Demo!]

Notice that the receiving page has to repeat the GetDigest() method as well as the secret salt. Also, the receiving page and initiating page must both agree upon what querystring values are considered tamper-proof. (One option to circumvent this problem is to always make all querystring parameters tamper-proof.) Regarding the duplication of the secret salt and GetDigest() method, an optimal approach would be to place this logic in a separate class library that could then be imported by the ASP.NET project and used from both the initiating and receiving page.

Beware the Replay Attack!
Acute reader Phil D. wrote in to warn about the potential of replay attacks with this system:
"If you are trying to use such a scheme as part of an authentication scheme, readers need to keep in mind that search engines would cache valid URL's and likewise authenticated users could cut and paste valid (ie. not tampered with) URL's and message them to other users. In that sense, you really aren't adding much additional security unless your salt changes very frequently and then you'd compromise usability/spider-ability."
Great point, Phil. Page persistence in search engines can be a security point, to be sure; see Security Alert - Using #includes Improperly from non-Debugged ASP Pages can allow Visitors to View your souce code, for instance.

One approach to circumventing the replay attack is to include in the tamper-proof querystring a time value that is included in the salt. The receiving page can then incorporate this time value when computing the hash. Furthermore, the receiving page would reject any request whose time parameter was more than, say, 30 seconds prior to the current time. While this approach provides more stringent security it does, as Phil notes, reduce usability and spider-ability. This topic is discussed in more detail in Creating Expiring Web Pages.

Conclusion


In this article we examined a technique for creating tamper-proof URLs. This objective was accomplished by using MD5, a one-way hash function, to create a digest of those querystring parameters that we wanted to be tamper-proof. To build a working tamper-proof URL system, both the initiating and receiving web pages must agree on the secret salt as well as the set of querystring parameters that are considered tamper-proof. The code examined in this article could be better utilized by refactoring it into a separate class library project.

Happy Programming!

  • By Scott Mitchell

  • Article Information
    Article Title: ASP.NET.Passing Tamper-Proof QueryString Parameters
    Article Author: Scott Mitchell
    Published Date: August 31, 2005
    Article URL: http://www.4GuysFromRolla.com/articles/083105-1.aspx


    Copyright 2014 QuinStreet Inc. All Rights Reserved.
    Legal Notices, Licensing, Permissions, Privacy Policy.
    Advertise | Newsletters | E-mail Offers