Securing image URLs in a website
How to hide image URLs on a website to avoid illegal access, using a custom HttpHandler and encryption.
Introduction
Recently, I started work on a medical image server for a hospital. Clearly, in such a context, security is very important. Because I had to authenticate users using some 3rd party components that are integrated into the HIS (hospital information system), Windows authentication was out of the question. This meant I could not use NTFS permissions to secure the actual image files. Form based authentication is possible, but does not solve my problem of someone typing in the URL of a patient's image directly. For simplicity, I stuck with anonymous IIS authentication, and a login form that created a session ticket which could be checked on every page. Again, this does not solve the problem of direct image access using URLs. However, I found some interesting articles about custom HTTP handlers on the MSDN site. This alone actually WORSENS the problem as it allows a user to actually gain access to images on the whole file system of the web server, but together with symmetric encryption (again from articles on the net), a secure system can be setup!
Taking the images out of the website
In order to avoid somebody typing in URLs to gain illegal access to images, we have to take them out of the website if we cannot rely on file permissions. In order to still be able to access those files, we will setup a custom HTTP handler (also check out Microsoft support and more specifically MSDN). This last article looked exactly what I needed, so I implemented it. Please read it for details.
In web.config, you add:
<httpHandlers><add verb="GET" path="ShowImage.axd"
type="Imageserver.StreamImage, ImageServer" />
</httpHandlers>
The page ShowImage.axd is a dummy endpoint, it doesn't actually exist. The type
tag points to a class in my namespace ImageServer
, which I will list further. HTML image elements now looked something like:
<img src="ShowImage.axd?Path='Some PHYSICAL path, e.g. C:\Images\Img1.jpg'" />
Encryption
However, to my amazement, this method actually allows a user to download an image file from anywhere in the file system of the server. Indeed, I dumped an image test.jpg in C:\temp\ on the server, typed in the URL "ShowImage.axd?Path=C:\temp\test.jpg" in IE, and PRESTO, it showed the test image! Clearly not an improvement, even if it can be solved with extra coding in the handler and NTFS permissions ...
After some reflection, I decided to use symmetric encryption to make the Path querystring unreadable on the client, thereby hiding any details about the location of my images on the server. This also avoids users trying to type in random paths, as the chance that a valid path results after decryption is very, very small. So the procedure goes as follows:
- Generate a key when a user logs on and store it in the session object.
- Encrypt any path querystring in the ASP.NET pages using that key.
- Decrypt the path querystring in the
StreamImage
HttpHandler
class associated with the ShowImage.axd endpoint. - Read the image from the file system and stream it to the client.
So finally the HTTP handler looks as follows (based on MSDN code!):
Public Class StreamImage
Implements IHttpHandler
Implements IReadOnlySessionState
Public ReadOnly Property IsReusable() As Boolean _
Implements IHttpHandler.IsReusable
Get
Return True
End Get
End Property
Private Sub WriteImage(ByVal ctx As HttpContext, ByVal FileName As String)
If FileName Is Nothing Then Return
Dim strContentType As String = "image/JPEG"
Dim ext As String = IO.Path.GetExtension(FileName).ToLower
Select Case ext
Case ".gif"
strContentType = "image/GIF"
Case ".png"
strContentType = "image/PNG"
End Select
ctx.Response.ContentType = strContentType
ctx.Response.WriteFile(FileName)
End Sub
Public Sub ProcessRequest(ByVal ctx As HttpContext) _
Implements IHttpHandler.ProcessRequest
'This sub uses 3DES to decrypt the image filename to avoid people typing
'in the URL to this handler directly, and thereby gaining read access to
'images on the WHOLE server filesystem!!!!!!!!!!!!!!!!
'Each session generates its own symmetric key
'which is stored in the session object
'This means that as long as the session object is safe there is no problem
Try
Dim strPath As String = ctx.Request.Params("Path"), strDecPath As String
Dim bOk As Boolean = False
ctx.Trace.Write("ProcessRequest", "Encrypted image path " & strPath)
If Not strPath Is Nothing Then
' TODO -- Add Role Check
strDecPath = Common.DecryptString(strPath, ctx.Session)
ctx.Trace.Write("ProcessRequest", "Decrypted image path " & strDecPath)
If Not strDecPath Is Nothing Then
If File.Exists(strDecPath) Then
bOk = True
Else
'ctx.Trace.Warn("ProcessRequest", _
' "Invalid image path after decryption!")
End If
Else
'ctx.Trace.Warn("ProcessRequest", "Encryption key or IV missing!")
End If
Else
'ctx.Trace.Warn("ProcessRequest", "Image path missing!")
End If
If bOk Then
Me.WriteImage(ctx, strDecPath)
Else
Me.WriteImage(ctx, ctx.Server.MapPath("/images/false.gif"))
End If
Catch ex As Exception
'ctx.Trace.Warn("ProcessRequest", "Runtime error in custom HTTP handler!")
End Try
End Sub
End Class
The routines for the encryption and decryption of strings look like this (modified from code snippets on the net):
Public Overloads Shared Function EncryptString(ByVal value As String,_
ByVal oPage As Page) As String
If Not oPage.Session("Key") Is Nothing And Not _
oPage.Session("Key") Is Nothing Then
Return EncryptString(value, oPage.Session("Key"), oPage.Session("IV"))
Else
Return Nothing
End If
End Function
Public Overloads Shared Function EncryptString(ByVal value_
As String, ByVal oKey() As Byte, ByVal oIV() As Byte) As String
If value <> "" Then
Dim oCryptoProvider As TripleDESCryptoServiceProvider = _
New TripleDESCryptoServiceProvider
Dim oMemoryStream As MemoryStream = New MemoryStream
Dim oCryptoStream As CryptoStream = _
New CryptoStream(oMemoryStream, oCryptoProvider.CreateEncryptor(oKey, oIV), _
CryptoStreamMode.Write)
Dim sw As StreamWriter = New StreamWriter(oCryptoStream)
sw.Write(value)
sw.Flush()
oCryptoStream.FlushFinalBlock()
oMemoryStream.Flush()
Return Convert.ToBase64String(oMemoryStream.GetBuffer(),_
0, oMemoryStream.Length)
End If
End Function
Public Overloads Shared Function DecryptString(ByVal value_
As String, ByVal oS As HttpSessionState) As String
If Not oS("Key") Is Nothing And Not oS("Key") Is Nothing Then
Return DecryptString(value, oS("Key"), oS("IV"))
Else
Return Nothing
End If
End Function
Public Overloads Shared Function DecryptString(ByVal value As String, _
ByVal oKey() As Byte, ByVal oIV() As Byte) As String
If value <> "" Then
Dim ooCryptoProvider As TripleDESCryptoServiceProvider_
= New TripleDESCryptoServiceProvider
Dim buffer As Byte() = Convert.FromBase64String(value)
Dim oMemStream As MemoryStream = New MemoryStream(buffer)
Dim oCryptoStream As CryptoStream = _
New CryptoStream(oMemStream, _
ooCryptoProvider.CreateDecryptor(oKey, oIV), CryptoStreamMode.Read)
Dim sr As StreamReader = New StreamReader(oCryptoStream)
Return sr.ReadToEnd()
Else
Return ""
End If
End Function
Some of the overloaded methods take into account that the personal symmetric key of a user is stored in the session object. Note that the querystring becomes almost twice as long because of the 2 byte per characters.
The correct URL for an image thus becomes (ASP style):
<img ID='ImageControl'
src='ShowImage.axd?Path=<% EncryptString("C:\Images\Img1.jpg", Page) %>' />
More likely the src
attribute will be set in the code-behind page:
Aspx page:
<img ID='ImageControl' runat='server' />
aspx.vb codebehind:
ImageControl.attribues.add("src", _
"ShowImage.axd?Path=" & EncryptString("C:\Images\Img1.jpg", Page))
Conclusion
We have presented a way to hide the URLs of images from any access outside that permitted by the security logic inside the aspx pages: no URLs in the HTML source code anymore, and no direct typing in of URLs either.