Introduction
In last week's article, Passing Tamper-Proof QueryString Parameters,
I illustrated one technique for passing tamper-proof querystring parameters from one Web page to another. (In reality, I've
not used the techniques discussed in last week's article to create tamper-proof URLs within a website; rather, I've used it
as a means to pass authentication information from one website to a partner website.) In my discussion in last week's article
I did omit one very important safe-guard for such systems: how to prevent replay attacks (thanks to alert 4Guys reader Phil D. for
pointing out this ommission).
The problem with the tamper-proof querystring parameter values approach I shared last week was that the tamper-proof check
does not do any sort of expiration check. That means once a URL has been created it is good forever. To see where this can
cause problems, imagine that we are using this technique to pass authentication information from one website (Site A) to a partner
website (Site B). Specifically, Site A sends to Site B a querystring that looks like: ?UserName=username&Digest=HashOfUsernameAlongWithSecretSalt.
Both websites will have to agree upon the secret salt and share that with one another, but as long as the end user is not
privy to this knowledge, they cannot craft their own, valid authenticating querystring parameters. If end user Sally clicks from
Site A onto Site B, she'll see the UserName parameter in the querystring. However, if she tries to alter it,
changing it from "Sally" to, say, "Maria", the receiving page's digest check will fail.
The problem of replay attacks remains, however. If Sally bookmarks the link from Site A to Site B, and her evil coworker
Theo finds this bookmark, Theo can then visit Site B authenticated as Sally. Theo can do this days, weeeks, or months after
Sally last visits Site B. What we'd like to do, is make that link from Site A to Site B only "active" for a short period
of time, say 60 seconds. That way, even if Sally bookmarks the link directly from Site A to Site B, if she - or anyone else -
visits that link more than 60 seconds after the link was created, they'll get an error on Site B, saying that the
link has expired.
In this article we'll see how to prevent against these types of replay attacks. The method examined can be used in the
authentication scenario just described, or can be used to creating "self-expiring" links. Read on to learn more!
Creating a Link That Expires Last week's article illustrated how to pass tamper-proof
querystring parameters (if you have yet to read that article, please do so before continuing on). To create a link that
expires, all we need to do is pass the time the link was created
in the querystring as a tamper-proof URL. The receiving page, then, can examine the time and determine whether or not the
link has expired. I prefer to let the receiving page determine when a link expires. However, if you trust the sender you
could adjust the algorithm to allow the sender to specify for how long the link is valid. In that case, rather than having
the sender create a tamper-proof querystring value with the time the link was created, you'd add a tamper-proof querystring
value indicating when the link expires.
Let's look at a very simple example. Imagine that I have a site with two pages, PageA.aspx and PageB.aspx.
I want to only allow people to visit PageB.aspx if they have first read through PageA.aspx and checked
an "I Have Read this Page" checkbox. (Clearly there are many ways to accomplish this, and using an "expiring page" might be
a bit of overkill, but it should illustrate the point nicely.) Let's first look at PageA.aspx:
PageA.aspx
<form runat="server">
Blah blah blah... boring legalese... <b>READ IT ALL!</b>
<p>
<asp:CheckBox runat="server" id="IHaveReadThis" Text="I Have Read this Page" />
<asp:Label runat="server" id="ReadMeMsg" Visible="False" ForeColor="Red"
Font-Italic="True">Please read this page and
check this checkbox...</asp:Label>
<br />
<asp:Button Runat="server" id="btnGoToPageB" Text="Go to PageB.aspx" />
</form>
<script runat="server" language="VB">
Sub GotoPageB(sender as Object, e as EventArgs)
'Make sure the checkbox is checked
If IHaveReadThis.Checked Then
'Redirect user to URL
Response.Redirect(CreateTamperProofURL("ExpiringDemoB.aspx", _
String.Empty, _
"Time=" & DateTime.Now.ToString("yyyyMMddHHmmss")))
Else
ReadMeMsg.Visible = True
End If
End Sub
... Code emitted for brevity, see live demo for complete code ...
</script>
This page presents the legalese (the "Blah blah blah" part) followed by a CheckBox and a Button Web control. When the
Button is clicked, a postback ensues and the GotoPageB event handler is executed. This event handler first
checks to ensure that the CheckBox has indeed been checked. If it has not, the user is kept on this page and
shown an error message. If the user has checked the CheckBox they are redirected via a Response.Redirect()
to ExpiringDemoB.aspx, passing in the querystring the current date/time formatted as the four digit year,
followed by the two-digit month, followed by the two-digit day of the month, followed by the two-digit hour as of a 24-hour clock,
followed by the two-digit minute, followed by the two-digit second. The CreateTamperProofURL() method, which
is omitted for brevity from the above code section, was discussed in detail in Passing
Tamper-Proof QueryString Parameters. As you'll recall from that article, CreateTamperProofURL() uses MD5
to create a digest of the tamper-proof querystring parameter(s) (Time, in this example).
That's all there is for PageA.aspx. All that's left is to create PageB.aspx, where we'll
parse the querystring and determine if request is too dated or not.
Creating the Receiving Page and Determining if the Request is Stale
In order to determine if the incoming request to PageB.aspx is stale, PageB.aspx needs to perform
the following checks:
Ensure that a Time querystring parameter is included,
Ensure that the Digest matches up with the Time value,
Determine the delta between the Time value in the querystring and the current date/time, and see
if that delta is greater than the expiry of the page.
This can be accomplished with the following code:
PageB.aspx
<h2><asp:Label runat="server" id="msg" /></h2>
<script runat="server" language="VB">
Sub Page_Load(sender as Object, e as EventArgs)
'Make sure the Time is provided
Dim t as String = Request.QueryString("Time")
If t is Nothing OrElse t.Length = 0 Then
Msg.Text = "ERROR: No time value was passed in the querystring"
Msg.ForeColor = System.Drawing.Color.Red
Else
'Make sure the digest matches up
EnsureURLNotTampered(String.Format("Time={0}", Request.QueryString("Time")))
'Finally, make sure that the time is within the 'legal' window
Dim reqTime As DateTime = DateTime.ParseExact(Request.QueryString("Time"), _
"yyyyMMddHHmmss", System.Globalization.DateTimeFormatInfo.InvariantInfo)
Dim RequestWindowInSeconds as Integer = 15
If Math.Abs(reqTime.Subtract(DateTime.Now).TotalSeconds) > RequestWindowInSeconds Then
Msg.Text = "ERROR: Link is stale!"
Msg.ForeColor = System.Drawing.Color.Red
Else
Msg.ForeColor = System.Drawing.Color.Black
Msg.Text = "Congrats, this is the protected message!"
End If
End If
End Sub
... Code emitted for brevity, see live demo for complete code ...
</script>
As with the previous code snippet, some of the code here has been omitted for brevity; refer to the live demo for the complete
source code and consult Passing
Tamper-Proof QueryString Parameters for an explanation as to how the receiving page ensures that the digest matches up.
As aforementioned, PageB.aspx does three things: first it makes sure that the Time querystring
parameter is provided; next, it makes a call to EnsureURLNotTampered() to verify that the user hasn't attempted to
tinker with the Time or Digest values; lastly, it reads in the Time value passed in
the querystring and determines how many seconds have elapsed between the passed-in time and the web server's current time.
If that delta is greater than 15 seconds, it displays a warning message, informing the user that the link has expired.
Real-World Considerations
The example just presented was designed to highlight the concept, not to make any claims as to the best practices for implementing
expiring URLs. For example, in the code snippets we just examined, all of the logic is placed squarely in each page. This
approach would be much less tedious to apply if the timestamp checking and expirary rules were moved to a
base class or, even better, an
HTTP Module.
Furthermore, the expiry for this example was set very low - 15 seconds - which is likely too short in real applications.
Users might be coming over dial-up modems, or there may be a hiccup between the client and your web server. In any case,
if it takes longer than 15 seconds between when the Response.Redirect() crafts the Time parameter
and when the client's request for that page reaches the web server, then the user will arrive at the linked to page seeing
the expirary message. Talk about a less than ideal user experience!
In this example the expiring page was arrived at from another web page in the same site. If you are using this technique
to pass information from one website to another, realize that these web servers may be in different time zones. The code
provided as-is does not take into affect any time zone differences when calculating the delta between the time the
querystring Time parameter and the current date/time. To overcome this, both web servers should agree to talk about
times in terms of UTC time. The DateTime structure has methods to convert local time to and from UTC time (see
ToUniversalTime()
and ToLocalTime()).
Additionally, different web servers may have a skew between their system clocks. (That is, the receiving web server might
think the UTC time is 8:23:08, but the sending time server's clock might be slow and think the UTC time is 08:21:52.)
Hopefully both are using some Internet-based time service to periodically update their clocks, but even so, between refreshes
there may be some drift. Hence, you must take care not to make the window that a link is valid too small.
Lastly, if you are creating such expiray links and placing them on web pages where they may be spidered by search engines,
the search engines may store the resulting page's URL using the time-expiring URL. This means that a potential visitor who
clicks on a search engine result may see the "This link has expired" message, which would be annoying for the user.
FYI, I have used this technique in two real-world applications I've worked on, both of which involved passing authentication
information from one website to a partner website. With the first project, the time window was rather short at about five minutes.
The system was used as follows: a person would visit Site A and on their Site A homepage there'd be a link to complete a certain
task that was handled by partner Site B. Clicking that link would build up a URL that included their authentication information
and the time the URL was created (in UTC time), sending it to the receiving web server. The receiving web server, then,
would verify the integrity of the digest and then make sure that the delta between the current time and sent time was no greater
than 10 minutes. The second project used this expiring URL to offer people a "window" during which they could sign up for
the site. A person would receive an email with instructions to sign up via a link provided in the email; the link in the email
would "expire" in 30 days, meaning that if the user attempted to go signup 30 or more days after receiving the email,
they'd be shown a message informing them that the opportunity they had to sign up had passed.
Conclusion
In this article we examined how to create expiring web pages. This feat is accomplished by creating tamper-proof querystring
parameters (a technique examined in a previous article) and sending along the time in the querystring. The receiving page,
then, is able to determine whether or not the link used to arrive at the page has expired or not based on some business logic.