Click here to Skip to main content
Click here to Skip to main content

Securing image URLs in a website

By , 21 Apr 2004
 

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:

  1. Generate a key when a user logs on and store it in the session object.
  2. Encrypt any path querystring in the ASP.NET pages using that key.
  3. Decrypt the path querystring in the StreamImage HttpHandler class associated with the ShowImage.axd endpoint.
  4. 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.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)

About the Author

yvdh
Software Developer (Senior)
Belgium Belgium
Member
Physicist, Biomedical Engineer, Phd in engineering. Specific expertise is in medical photography and it's related image processing, and in colourimetry.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
Generalhelpmemberannnnnnnnnnnnnnnn20 Jan '06 - 3:07 
Hi can anyone help
 
got the handler stuff working but the images I want to display are remote images via http://domain/images.gif" how can I get this working
 
Frown | :(
GeneralRe: help Pinmemberyvdh20 Jan '06 - 4:04 
Hello,
 
I suppose that the webserver that is serving up the images is not generally accessible, otherwise this defeats the whole purpose of the article: a user could just type in the url http://domain/images.gif and get them directly!
 
If this is ok, then you could use a webclient in the http handler to get at the images, instead of just reading the files...
 
I will need more details to answer yout question better!
 
Yves

GeneralRe: help Pinmemberannnnnnnnnnnnnnnn20 Jan '06 - 4:09 
Sniff | :^) Hi I got it working by doing a response.redirect and thats all ok, but now on the last bit, its encrypting the url, but not doing anything else like displaying the image, have to see if the process code is being actioned dont think it is
GeneralRe: help Pinmemberannnnnnnnnnnnnnnn20 Jan '06 - 4:14 
:(for some reason its getting here
 
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
 

but witgh the encrypted url but the key is nothing..
GeneralRe: help Pinmemberannnnnnnnnnnnnnnn20 Jan '06 - 4:18 
what should objPage actually be declared as
 
objPage.Session("Key") = o3DES.Key
objPage.Session("IV") = o3DES.IV
 

-- modified at 10:18 Friday 20th January, 2006
GeneralRe: help Pinmemberyvdh20 Jan '06 - 6:06 
Whats the question here? I suppose you are talking about the routine that puts the encryption keys and IV in the session object... Just pass on 'Page' (the intrinsic variable of each aspx page).
 
Doing a response redirect is fine, but the method I use allows a stream to be directed to an img control. This means it can be used in a img control directly, so as not to give the location of your images away.
 
Yves

GeneralRe: help Pinmemberannnnnnnnnnnnnnnn23 Jan '06 - 0:26 
Hi just to let you know I got it wortking with remote image and non images url paths as well as local paths.. I had to alter the code to do a response.redirect rather than write file and content type plus I had to UrlEncode tha path as thats what was making it work once only when ever i tried it.
 
the only question I have now is this. The key for example on a per page basis I store in the session object, if though i wentr to the url as I did view source as saw showimage.axd, I can at the moment type in showimage.axd path gdgdggdgdgdggdggdggd etc directly into the url while on that page and hey presto it shows the real path. how would you suggest to control this.. or would it be wise if its possible to make the session key only valid for that page bu removing it at the end of the page build... so it does not exist.
GeneralRe: help Pinmemberyvdh23 Jan '06 - 1:07 
Because you use an URL on a second webserver instead of a file stored on the primary webserver (which is not accesible by URL), and you redirect to it, it is logical the user can see the path, and thus 'steal' images at will by typing in URLs for that second webserver once he knows where to look. I think (but haven't tried) there are several solutions for this:
 
- stick to the filesystem idea. For this you could use a secured network share to acces the images on the secondary webserver from the primary webserver, instead of using URL's. This is probably not an option for your setup ...
 
- Instead of doing a redirect, you should use the webclient (or WebRequest) class to obtain a stream of bytes from the secondary server, and inject it into the response object. Doing this alone will already hide the second webserver from the user, but that's not much of a security, so also secure the secondary webserver, e.g. making it only accessible from the primary one or by using authentication. Im doing something similar in some of my other classes (httphandlers for smartclient imaging apps), I'll check it out when I am at work and send some code.
 
Hope this helps
Yves

GeneralRe: help Pinmemberannnnnnnnnnnnnnnn23 Jan '06 - 1:16 
The 2nd webserver is not my webserver, its a thirdparty client, so I have no control on that box. The local path sounds like a good idea for mapping it, so I may have a go at that. Thanks
GeneralRe: help Pinmemberyvdh23 Jan '06 - 2:03 
Mmm,
 
there is something I don't understand then: if their webserver is unsecured, what's the problem then? Anybody can find it and get the images, or not? I you can give me more details about the exact network topology (intranet - internet, relation fo webserver 1 verssus webserver 2, etc. ..)I might be of more assistance.
GeneralRe: help Pinmemberannnnnnnnnnnnnnnn23 Jan '06 - 2:22 
OMG | :OMG: Hi, yes anyone could actually type in the url and look at the images. Its something I don't agree with, but because they are preview images then it does not matter and the word preview is all over them. All I really wanted to do was just hide the url from the view source etc, so if a competitor looked at the urls to see who we were using they would not be able to find out with ease.
 
It all kinda works, though apart from a couple of small things on some different files, but they dont matter.
 
All I want to do now is sort out the problem where it shows the actual location.. This I think may mean I need to do some streaming work, but dont have time for that now.
 

 
I'll look at it now for a mo... just so the url does not change on the redirect,, I'm nto sure but either stream or server.transfer may do the trick.
GeneralRe: help Pinmemberannnnnnnnnnnnnnnn23 Jan '06 - 2:25 
thanks

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 22 Apr 2004
Article Copyright 2004 by yvdh
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid