Question

I'm considering developing an app for Google App Engine, which should not get too much traffic. I'd really rather not pay to exceed the free quotas. However, it seems like it would be quite easy to cause a denial of service attack by overloading the app and exceeding the quotas. Are there any methods to prevent or make it harder to exceed the free quotas? I know I could, for example, limit the number of requests from an IP (making it harder to exceed the CPU quota), but is there any way to make it harder to exceed the requests or bandwidth quotas?

Was it helpful?

Solution

There are no built-in tools to prevent DoS. If you are writing Google Apps using java then you can use the service.FloodFilter filter. The following piece of code will execute before any of your Servlets do.

package service;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;


/**
 * 
 * This filter can protect web server from simple DoS attacks
 * via request flooding.
 * 
 * It can limit a number of simultaneously processing requests
 * from one ip and requests to one page.
 *
 * To use filter add this lines to your web.xml file in a <web-app> section.
 * 
    <filter>
        <filter-name>FloodFilter</filter-name>
        <filter-class>service.FloodFilter</filter-class>
        <init-param>
            <param-name>maxPageRequests</param-name>
            <param-value>50</param-value>
        </init-param>
        <init-param>
            <param-name>maxClientRequests</param-name>
            <param-value>5</param-value>
        </init-param>
        <init-param>
            <param-name>busyPage</param-name>
            <param-value>/busy.html</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>JSP flood filter</filter-name>
        <url-pattern>*.jsp</url-pattern>
    </filter-mapping>
 *  
 *  PARAMETERS
 *  
 *  maxPageRequests:    limits simultaneous requests to every page
 *  maxClientRequests:  limits simultaneous requests from one client (ip)
 *  busyPage:           busy page to send to client if the limit is exceeded
 *                      this page MUST NOT be intercepted by this filter
 *  
 */
public class FloodFilter implements Filter
{
    private Map <String, Integer> pageRequests;
    private Map <String, Integer> clientRequests;

    private ServletContext context;
    private int maxPageRequests = 50;
    private int maxClientRequests = 10;
    private String busyPage = "/busy.html";


    public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException
    {
        String page = null;
        String ip = null;

        try {
            if ( request instanceof HttpServletRequest ) {
                // obtaining client ip and page URI without parameters & jsessionid
                HttpServletRequest req = (HttpServletRequest) request;
                page = req.getRequestURI();

                if ( page.indexOf( ';' ) >= 0 )
                    page = page.substring( 0, page.indexOf( ';' ) );

                ip = req.getRemoteAddr();

                // trying & registering request
                if ( !tryRequest( page, ip ) ) {
                    // too many requests in process (from one client or for this page) 
                    context.log( "Flood denied from "+ip+" on page "+page );
                    page = null;
                    // forwarding to busy page
                    context.getRequestDispatcher( busyPage ).forward( request, response );
                    return;
                }
            }

            // requesting next filter or servlet
            chain.doFilter( request, response );
        } finally {
            if ( page != null )
                // unregistering the request
                releaseRequest( page, ip );
        }
    }


    private synchronized boolean tryRequest( String page, String ip )
    {
        // checking page requests
        Integer pNum = pageRequests.get( page );

        if ( pNum == null )
            pNum = 1;
        else {
            if ( pNum > maxPageRequests )
                return false;

            pNum = pNum + 1;
        }

        // checking client requests
        Integer cNum = clientRequests.get( ip );

        if ( cNum == null )
            cNum = 1;
        else {
            if ( cNum > maxClientRequests )
                return false;

            cNum = cNum + 1;
        }

        pageRequests.put( page, pNum );
        clientRequests.put( ip, cNum );

        return true;
    }


    private synchronized void releaseRequest( String page, String ip )
    {
        // removing page request
        Integer pNum = pageRequests.get( page );

        if ( pNum == null ) return;

        if ( pNum <= 1 )
            pageRequests.remove( page );
        else
            pageRequests.put( page, pNum-1 );

        // removing client request
        Integer cNum = clientRequests.get( ip );

        if ( cNum == null ) return;

        if ( cNum <= 1 )
            clientRequests.remove( ip );
        else
            clientRequests.put( ip, cNum-1 );
    }


    public synchronized void init( FilterConfig config ) throws ServletException
    {
        // configuring filter
        this.context = config.getServletContext();
        pageRequests = new HashMap <String,Integer> ();
        clientRequests = new HashMap <String,Integer> ();
        String s = config.getInitParameter( "maxPageRequests" );

        if ( s != null ) 
            maxPageRequests = Integer.parseInt( s );

        s = config.getInitParameter( "maxClientRequests" );

        if ( s != null ) 
            maxClientRequests = Integer.parseInt( s );

        s = config.getInitParameter( "busyPage" );

        if ( s != null ) 
            busyPage = s;
    }


    public synchronized void destroy()
    {
        pageRequests.clear();
        clientRequests.clear();
    }
}

If you are using python, then you may have to roll your own filter.

OTHER TIPS

I'm not sure if it's possible, but the App Engine FAQs indicate that if you can show it's a DOS attack then they'll refund any fees associated with the attack.

It seems that they have an IP-address based filter available for both Python and Java now (I know this is an old thread, but it still comes up high on a Google search).

https://developers.google.com/appengine/docs/python/config/dos

It's always possible to use a service that provides Denial of Service protection features in front of an App Engine application. For example, Cloudflare provides a well respected service https://www.cloudflare.com/waf/, and there are others. It's my understanding (disclaimer: I haven't used the service personally) that these features are available on the free plan.

It's also fairly easy to construct a memcache based rate limiting implementation in your application itself. Here's the first hit I got from a google search for this method: http://blog.simonwillison.net/post/57956846132/ratelimitcache. This mechanism is sound, and can be cost effective as shared memcache usage may suffice and is free. Furthermore, going this route puts you in control of the knobs. The drawback is that the application itself must handle the HTTP request and decide to allow or deny it, so there may be cost (or [free] quota exhaustion) to deal with.

Full Disclosure: I work at Google on App Engine, and have no association with Cloudflare or Simon Willison.

The GAE firewall was recently released, intended to replace the previous, rather limited, DoS Protection Service.

It supports programmatic updates of the firewall rules via the (REST) Admin API: apps.firewall.ingressRules which could be combined with an in-app piece of logic for DoS detection as described in other answers. The difference would be that once the rule is deployed the offending requests will no longer incur charges as they don't reach the app anymore, so the in-app filtering itself is not needed.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top