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:
- The secret salt, and
- The list of querystring parameters that should be tamper-proof
|
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.
|
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!