I encountered some strange behavior while implementing role-based security. The web application would get stuck in an endless loop at around 16h00 every day. This only happened while running on the web server and could not be reproduced locally in a development environment.

The problem was that the Central Authentication Service (CAS) would invalidate the session tokens after 8 hours. And since most people start at 08h00, the problem would only manifest after 16h00. At this point, the authentication system and the response filter started a tug o’ war – the authentication system trying to redirect the response to the login page and the response filter trying to flush the filtered response back to the client.

Special thanks to Mike D for figuring it out, without even looking at a single line of code… I had to hand over one of my hats.

The solution was simple – to bypass the filter if the response is being redirected (301), line 11:

public class WebApplication : System.Web.HttpApplication
{
    public WebApplication()
    {
        ReleaseRequestState += new EventHandler(OnReleaseRequestState);
    }

    void OnReleaseRequestState(object sender, EventArgs e)
    {
        // Ensure that the request is not being redirected before applying the filter
        if (HttpContext.Current.Response.StatusCode == (int)HttpStatusCode.Redirect)
            return;

        // Install the filter
        var response = HttpContext.Current.Response;
        var request = HttpContext.Current.Request;
        response.Filter = new SecurityFilterStream(response.Filter, request);
    }
}

Furthermore, the server was not returning the entire page through the filter, but cutting it off after 8 KB. It was buffering the response. This also only happened on the server and could not be reproduced in a development environment. The solution was to write to a cached memory stream and only modify it during the flush operation, instead of the more conventional way of modifying the stream during the write operation:
public class SecurityFilterStream : Stream
{
    Stream Sink;
    HttpRequest Request;
    MemoryStream CachedStream = new MemoryStream();
    bool IsClosing;
    bool IsClosed;

    public SecurityFilterStream(Stream sink, HttpRequest request)
    {
        Sink = sink;
        Request = request;
    }

    public override void Close()
    {
        IsClosing = true;
        Flush();
        IsClosed = true;
        IsClosing = false;
        Sink.Close();
    }

    public override void Flush()
    {
        if (IsClosing && !IsClosed)
        {
            // Get the response string from the stream
            Encoding encoding = HttpContext.Current.Response.ContentEncoding;
            string responseString = encoding.GetString(CachedStream.ToArray());
            string filteredString = "";

            // Loop through the response and alter the markup in individual elements
            using (StringReader reader = new StringReader(responseString))
            {
                string line = string.Empty;
                do
                {
                    line = reader.ReadLine();
                    if (line != null && line.Contains("sec-function"))
                    {
                        // [...]
                        filteredString += line + "\r\n";
                    }
                } while (line != null);
            }

            // Overwrite the response with the altered string
            filteredString = filteredString.TrimEnd('\n').TrimEnd('\r');
            byte[] buffer = encoding.GetBytes(filteredString);
            Sink.Write(buffer, 0, buffer.Length);

            CachedStream = new MemoryStream();
            Sink.Flush();
        }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        return Sink.Read(buffer, offset, count);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        CachedStream.Write(buffer, 0, count);
    }
}
Advertisements