HOWTO: ISAPI Filter which rejects requests from SF_NOTIFY_PREPROC_HEADERS based on HTTP Referer

There was a recent question about how to reject requests (based on the HTTP Referer header in this case) in SF_NOTIFY_PREPROC_HEADERS.

Question:

Hello everyone,

I'm trying to solve a problem of referer spam: People will fake requests to the web server using online casino, "drug" retailers and other questionable web sites as referer. Not only does that messes up my web stats but I also happen to run a blog software that keeps track of these referer and actually display them to users.

Anyway, in order to solve that problem, I wrote a small ISAPI filter in Delphi 6. it works well so far with one problem: when it blocks a request, it sends back a 401.4 response: "A filter denied access to this resource".

I would like to send back a "403" which has a better chance to be properly interpreted as a "get out of my yard, buddy".

What I'm doing now:

1/ Calling SetLastError with ERROR_ACCESS_DENIED as parameter
2/ Returning SF_STATUS_REQ_ERROR as the result of the filter callback.

Since I'm filtering the even as soon as possible, when I get a SF_NOTIFY_PREPROC_HEADERS notification, I don't seem to have access to the return code itself.

Is there a way to send back that 403 without modifing to filter to run in the SF_NOTIFY_END_OF_REQUEST notification ? I'd like to minimize the impact of the request on the server and I see no reason to run multiple DB queries to generate a web page that will be truncated by the filter.

Thank you,

Answer:

Unfortunately, for an ISAPI Filter, there is no API call to send an IIS-configured custom error page. The same for ISAPI Extension until in IIS 6.0 when the HSE_REQ_SEND_CUSTOM_ERROR ServerSupportFunction was introduced to allow it to send IIS-configured custom error page by explicitly specifying HTTP status/sub-status code.

However, ISAPI Filter does have an "arbitrary" behavior which can cause a couple of HTTP responses to be sent when you call SetLastError() with the following values and return SF_STATUS_REQ_ERROR from SF_NOTIFY_PREPROC_HEADERS.

  1. ERROR_FILE_NOT_FOUND - returns 404 error
  2. ERROR_PATH_NOT_FOUND - returns 404 error
  3. ERROR_ACCESS_DENIED - returns 401.4 error
  4. Any other Win32 error code - returns 500 error with FormatMessage of the error code

Personally, from a security perspective, I would send back a 404 response using ERROR_FILE_NOT_FOUND instead of sending back 401.4 (tells attacker that you have an ISAPI Filter looking at responses, which can induce more attacks) or 403 (tells attacker that they are doing something the server does not want to do).

Of course, if you must send back the 403, you can certainly call SF_REQ_SEND_RESPONSE_HEADER with a 403 status string and return SF_REQ_STATUS_FINISHED from SF_NOTIFY_PREPROC_HEADERS, which would be just as efficient.

For example, UrlScan does this - SetLastError( ERROR_FILE_NOT_FOUND ) and return SF_REQ_STATUS_FINISHED from SF_NOTIFY_PREPROC_HEADERS for the fast path rejection.

Regarding your other question about "when I get a SF_NOTIFY_PREPREC_HEADERS notification, I don't seem to have access to the return code itself". I am not certain what return code you are referring to, but it is probably not needed.

There is no "return code" in SF_NOTIFY_PREPROC_HEADERS because it happens as IIS is parsing the request headers and before processing the request. Since an ISAPI Filter can write arbitrary data via WriteClient or SF_REQ_SEND_RESPONSE_HEADER to the client and terminate the request processing right then and there, you really do not need access to any return code. You have control of the entire request/response right then and there.

Also, rejecting a request in SF_NOTIFY_END_OF_REQUEST is not optimal. You have already done all the request processing, and you also need to consume response data in SF_NOTIFY_SEND_RAW_DATA (so that the client gets a proper HTTP response) as well as send the entire rejection response in SF_NOTIFY_END_OF_REQUEST. Too much work for a rejection.

Here is a complete filter sample in C (sorry, I really do not know Delphi) that requires that all requests have a Referer header whose value starts with "https://myserver". It has a whole bunch of comments that show how to send a 404 or a 403 response as well as how to filter on the Referer header.

Enjoy.

//David

 #include <windows.h>
#include <httpfilt.h>

#define DEFAULT_BUFFER_SIZE         1024
#define MAX_BUFFER_SIZE             4096

DWORD
OnPreprocHeaders(
    IN HTTP_FILTER_CONTEXT *            pfc,
    IN HTTP_FILTER_PREPROC_HEADERS *    pPPH
);

BOOL
WINAPI
GetFilterVersion(
    HTTP_FILTER_VERSION *           pVer
    )
/*++

Purpose:

    Required entry point for ISAPI filters.  This function
    is called when the server initially loads this DLL.

Arguments:

    pVer - Points to the filter version info structure

Returns:

    TRUE on successful initialization
    FALSE on initialization failure

--*/
{
    pVer->dwFilterVersion = HTTP_FILTER_REVISION;
    lstrcpyn( pVer->lpszFilterDesc,
             "Filter to reject based on HTTP Referer header",
             SF_MAX_FILTER_DESC_LEN );
    pVer->dwFlags =
        SF_NOTIFY_ORDER_HIGH |
        SF_NOTIFY_PREPROC_HEADERS
        ;

    return TRUE;
}

DWORD
WINAPI
HttpFilterProc(
    IN HTTP_FILTER_CONTEXT *        pfc,
    DWORD                           dwNotificationType,
    LPVOID                          pvNotification
    )
/*++

Purpose:

    Required filter notification entry point.  This function is called
    whenever one of the events (as registered in GetFilterVersion) occurs.

Arguments:

    pfc              - A pointer to the filter context for this notification
    NotificationType - The type of notification
    pvNotification   - A pointer to the notification data

Returns:

    One of the following valid filter return codes:
    - SF_STATUS_REQ_FINISHED
    - SF_STATUS_REQ_FINISHED_KEEP_CONN
    - SF_STATUS_REQ_NEXT_NOTIFICATION
    - SF_STATUS_REQ_HANDLED_NOTIFICATION
    - SF_STATUS_REQ_ERROR
    - SF_STATUS_REQ_READ_NEXT

--*/
{
    switch ( dwNotificationType )
    {
    case SF_NOTIFY_PREPROC_HEADERS:
        return OnPreprocHeaders(
            pfc,
            (HTTP_FILTER_PREPROC_HEADERS *) pvNotification );
    }

    return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD
OnPreprocHeaders(
    HTTP_FILTER_CONTEXT *           pfc,
    HTTP_FILTER_PREPROC_HEADERS *   pPPH
)
{
    DWORD                           dwRet = SF_STATUS_REQ_NEXT_NOTIFICATION;
    BOOL                            fRet = FALSE;
    CHAR                            pBuf[ DEFAULT_BUFFER_SIZE ];
    CHAR *                          pszBuf = pBuf;
    DWORD                           cbBuf = DEFAULT_BUFFER_SIZE;
    CHAR                            szReferer[] = "Referer:";
    CHAR                            szMyServerABS[] = "https://myserver";
    CHAR                            cbMyServerABS = 15;

    SetLastError( NO_ERROR );

    if ( pfc == NULL ||
         pPPH == NULL )
    {
        SetLastError( ERROR_INVALID_PARAMETER );
        goto Finished;
    }

    fRet = pPPH->GetHeader( pfc, szReferer, pszBuf, &cbBuf );
    if ( fRet == FALSE )
    {
        if ( GetLastError() == ERROR_INVALID_INDEX )
        {
            //
            // Referer header was not given
            // Two options:
            //
            // If not having Referer header is ok.
            //
            // OutputDebugString( "Nothing to do.\n" );
            // SetLastError( NO_ERROR );
            //
            // If Referer header is required
            //
            // OutputDebugString( "Referer header was not found.\n" );
            // SetLastError( ERROR_ACCESS_DENIED );
            //
            goto Finished;
        }
        else if ( GetLastError() == ERROR_INSUFFICIENT_BUFFER &&
                  cbBuf < MAX_BUFFER_SIZE )
        {
            pszBuf = new CHAR[ cbBuf ];
            if ( pszBuf == NULL )
            {
                SetLastError( ERROR_NOT_ENOUGH_MEMORY );
                goto Finished;
            }

            fRet = pPPH->GetHeader( pfc, szReferer, pszBuf, &cbBuf );
            if ( fRet == FALSE )
            {
                goto Finished;
            }

            OutputDebugString( "Found Referer header.\n" );
         }
         else
         {
             goto Finished;
         }
    }

    //
    // At this point, pszBuf points to the value of Referer header.
    // Figure out what to do.
    //
    OutputDebugString( pszBuf );
    OutputDebugString( "\n" );

    //
    // If it is not an absolute URL from my server, reject it
    //
    if ( _strnicmp( pszBuf, szMyServerABS, cbMyServerABS ) != 0 )
    {
        //
        // Option 1: Send a 404 response
        //
        // SetLastError( ERROR_FILE_NOT_FOUND );
        // goto Finished;
        //
        // Options 2: Send a 403 response
        //
        fRet = pfc->ServerSupportFunction( pfc,
                                           SF_REQ_SEND_RESPONSE_HEADER,
                                           (PVOID)"403 Forbidden",
                                           (ULONG_PTR)"Content-Length: 0\r\n"
                                           "Content-Type: text/html\r\n\r\n",
                                           NULL );
        if ( fRet == FALSE )
        {
            goto Finished;
        }

        dwRet = SF_STATUS_REQ_FINISHED;
    }

    SetLastError( NO_ERROR );

Finished:

    if ( pszBuf != pBuf )
    {
        delete pszBuf;
    }

    if ( GetLastError() != NO_ERROR )
    {
        OutputDebugString( "Error!\n" );
        dwRet = SF_STATUS_REQ_ERROR;
    }

    return dwRet;
}