Returning "Unsafe" files in NancyFX

When playing with Nancy I am often pleasantly surprise by how easy it is to extend. Often it is not just a case of “how easy” but “choose the extension point you would like to use". Streaming “Unsafe” files is one such scenario.

Nancy considers “unsafe” files to be any file existing outside of the current directory of the application. So if you ever try to return an unsafe file with Nancy you will receive a 404 “Not Found”. Nancy does this to reduce the chance of you application being hacked.

In my case I needed to return files from a specific network share. In Nancy there are several ways of doing this based on your specific requirements.

Custom RootPathProvider

Nancy stores the current “safe” directory path in the static variable GenericFileResponse.RootPath. All file paths must exist inside this path to be returned. This variable is initialised using the interface IRootPathProvider. So if you want to override this path simply implement your own IRootPathProvider and return your custom path.

public class RootPathProvider : IRootPathProvider
{
    public string GetRootPath()
    {
        return @"C:\PathWithFilesToShare";
    }
}

This method is good if you want to read all files from another specific location. It is not so good if you want to return files from multiple different locations. For that there are other methods.

Adding a Stream Response

So rather than handing over the file to Nancy process you can give Nancy a specific stream to return. This way no "safe path" checking will be done. This is done using two simple classes

public static class FormatterExtensions
{
    public static Response AsStream(this IResponseFormatter formatter, Func readStream, string contentType)
    {
        return new StreamResponse(readStream, contentType);
    }
}

public class StreamResponse : Response
{
    public StreamResponse(Func readStream, string contentType)
    {
        Contents = stream =>
                       {
                           using (var read = readStream())
                           {
                               read.CopyTo(stream);
                           }
                       };
        ContentType = contentType;
        StatusCode = HttpStatusCode.OK;
    }
}

Now in my module I can write

public class MainModule : NancyModule
{
    public MainModule()
    {
        Get["/"] = o =>
                       {
                           return Response.AsStream(() => File.OpenRead(@"C:\PathWithFilesToShare\FileName.txt"), "text/plain");
                       };
    }
}

Note: This approach removes a useful safety feature of Nancy and you should be careful of how you use it. If you are ever using request parameters to determining the file path you should add code to validate that path before returning it. Otherwise you will leave yourself open being hacked by people passing paths similar to "../../" and getting the contents of undesirable files.

Supporting Multiple Root Paths

The other approach involves cloning a fair amount of Nancy code to support multiple Root Paths. While this does involve more code it allow you to return files from multiple paths without losing the safety check.

Custom GenericFileResponse

GenericFileResponse is duplicated to GenericFileResponseEx and changed so RootPath is a List of strings. (See source code for full changes)

public class GenericFileResponseEx : Response
{
    public static List RootPaths { get; set; }
    ....
}

Custom FormatterExtensions

The FormatterExtensions is duplicated to provide usable extension point.

public static class FormatterExtensions
{
    public static Response AsFileEx(this IResponseFormatter formatter, string applicationRelativeFilePath, string contentType)
    {
        return new GenericFileResponseEx(applicationRelativeFilePath, contentType);
    }
    ...
}

Custom IStartup

The multiple Paths are configured.

public class RootPathStartup : IStartup
{
    public RootPathStartup(IRootPathProvider rootPathProvider)
    {
        GenericFileResponseEx.RootPaths.Add(rootPathProvider.GetRootPath());
        GenericFileResponseEx.RootPaths.Add(@"C:\PathWithFilesToShare");
        //TODO: add as many paths as you want to return files.
    }
    ...
}

Module code

Now in my module I can write this

public class MainModule : NancyModule
{
    public MainModule()
    {
        Get["/"] = o =>
        {
            return Response.AsFileEx("FileName.txt");
        };
    }
}

And the various root paths will be searched for FileName.txt.

Summary

So as you can see despite Nancy being a very minimalist framework it is also easily extended. There are also other variations on the above approaches. So feel free to experiment and find something that suits your specific requirements.

Sample Code

There is a full working samples here:

http://simonsexperiments.googlecode.com/hg/NancyUnSafeFiles

Posted by: Simon Cropp
Last revised: 24 Dec, 2011 05:20 AM History

Comments

No comments yet. Be the first!

No new comments are allowed on this post.