Code Monkey home page Code Monkey logo

play-guard's Introduction

Play Framework Guard Module

Maven Maven Maven Maven Maven

Play module for blocking and throttling abusive requests.

  • throttling specific Actions based on request attributes (e.g. IP address)

  • throttling specific Actions based on request attributes (e.g. IP address) and failure rate (e.g. HTTP status or any other Result attribute)

  • global IP address whitelisting/blacklisting

  • global request throttling

Target

This module targets the Scala version of Play 2.x.x and 3.x.x

Rate Limit Algorithm

Based on the token bucket algorithm: http://en.wikipedia.org/wiki/Token_bucket

Getting play-guard

For Play 3.0.x:

  "com.digitaltangible" %% "play-guard" % "3.0.0"

For Play 2.9.x:

  "com.digitaltangible" %% "play-guard" % "2.6.0"

For Play 2.8.x:

  "com.digitaltangible" %% "play-guard" % "2.5.0"

For Play 2.7.x:

  "com.digitaltangible" %% "play-guard" % "2.4.0"

For Play 2.6.x:

  "com.digitaltangible" %% "play-guard" % "2.2.0"

For Play 2.5.x:

  "com.digitaltangible" %% "play-guard" % "2.0.0"

For Play 2.4.x:

  "com.digitaltangible" %% "play-guard" % "1.6.0"

For Play 2.3.x:

  "com.digitaltangible" %% "play-guard" % "1.4.1"

1. RateLimitAction

Action function/filter for request and failure rate limiting specific actions. You can derive the bucket key from the request.

The rate limit functions/filters all take a RateLimiter instance as the first parameter:

class RateLimiter(val size: Long, val rate: Double, logPrefix: String = "", clock: Clock = CurrentTimeClock)

It holds the token bucket group with the specified size and rate and can be shared between actions if you want to use the same bucket group for various actions.

1.1 Request rate limit

There is a general ActionFilter for handling any type of request so you can chain it behind you own ActionTransformer:

/**
 * ActionFilter which holds a RateLimiter with a bucket for each key returned by `keyFromRequest`.
 * Can be used with any Request type. Useful if you want to use content from a wrapped request, e.g. User ID
 *
 * @param rateLimiter
 * @tparam R
 */
abstract class RateLimitActionFilter[R[_] <: Request[_]](rateLimiter: RateLimiter)(
  implicit val executionContext: ExecutionContext
) extends ActionFilter[R] {

  def keyFromRequest[A](implicit request: R[A]): Any

  def rejectResponse[A](implicit request: R[A]): Future[Result]

  def bypass[A](implicit request: R[A]): Boolean = false
  
  // ...
}

There are also two convenience filters:

IP address as key (from the sample app):

// allow 3 requests immediately and get a new token every 5 seconds
private val ipRateLimitFilter: IpRateLimitFilter[Request] = new IpRateLimitFilter[Request](new RateLimiter(3, 1f / 5, "test limit by IP address")) {
  override def rejectResponse[A](implicit request: Request[A]): Future[Result] =
    Future.successful(TooManyRequests(s"""rate limit for ${request.remoteAddress} exceeded"""))
}

def limitedByIp: Action[AnyContent] = (Action andThen ipRateLimitFilter) {
  Ok("limited by IP")
}

Action parameter as key (from the sample app):

// allow 4 requests immediately and get a new token every 15 seconds
private val keyRateLimitFilter: KeyRateLimitFilter[String, Request] =
  new KeyRateLimitFilter[String, Request](new RateLimiter(4, 1f / 15, "test by token")) {
    override def rejectResponse4Key[A](key: String): Request[A] => Future[Result] =
      _ => Future.successful(TooManyRequests(s"""rate limit for '$key' exceeded"""))
  }

def limitedByKey(key: String): Action[AnyContent] =
  (Action andThen keyRateLimitFilter(key)) {
    Ok("limited by token")
  }

1.2 Error rate limit

There is a general ActionFunction for handling any type of request so you can chain it behind your own ActionTransformer and determine failure from the Result:

/**
 * ActionFunction which holds a RateLimiter with a bucket for each key returned by method keyFromRequest.
 * Tokens are consumed only by failures determined by function resultCheck. If no tokens remain, requests with this key are rejected.
 * Can be used with any Request type. Useful if you want to use content from a wrapped request, e.g. User ID
 *
 * @param rateLimiter
 * @param resultCheck
 * @param executionContext
 * @tparam R
 */
abstract class FailureRateLimitFunction[R[_] <: Request[_]](
     rateLimiter: RateLimiter,
     resultCheck: Result => Boolean,
)(implicit val executionContext: ExecutionContext)
  extends ActionFunction[R, R] {

  def keyFromRequest[A](implicit request: R[A]): Any

  def rejectResponse[A](implicit request: R[A]): Future[Result]

  def bypass[A](implicit request: R[A]): Boolean = false
  
  // ...
}

The convenience action HttpErrorRateLimitAction limits the HTTP error rate for each IP address. This is for example useful if you want to prevent brute force bot attacks on authentication requests.

From the sample app:

// allow 2 failures immediately and get a new token every 10 seconds
private val httpErrorRateLimitFunction: HttpErrorRateLimitFunction[Request] =
new HttpErrorRateLimitFunction[Request](new RateLimiter(2, 1f / 10, "test failure rate limit")) {
  override def rejectResponse[A](implicit request: Request[A]): Future[Result] = Future.successful(BadRequest("failure rate exceeded"))
}

def failureLimitedByIp(fail: Boolean): Action[AnyContent] =
(Action andThen httpErrorRateLimitFunction) {
  if (fail) BadRequest("failed")
  else Ok("Ok")
}

1.3 Integration with Silhouette

https://www.silhouette.rocks/docs/rate-limiting

2. GuardFilter

Filter for global rate limiting and IP address whitelisting/blacklisting.

Note: this global filter is only useful if you don't have access to a reverse proxy like nginx where you can handle these kind of things

2.1 Rules

Rejects requests based on the following rules:

if IP address is in whitelist => let pass
else if IP address is in blacklist => reject with ‘403 FORBIDDEN’
else if IP address rate limit exceeded => reject with ‘429 TOO_MANY_REQUEST’
else if global rate limit exceeded => reject with ‘429 TOO_MANY_REQUEST’

2.2 Usage

For compile time DI:

class ApplicationComponents(context: Context) extends BuiltInComponentsFromContext(context) with PlayGuardComponents {

  override lazy val httpFilters: Seq[EssentialFilter] = Seq(guardFilter)
}

Runtime DI with Guice:

@Singleton
class Filters @Inject()(env: Environment, guardFilter: GuardFilter) extends DefaultHttpFilters(guardFilter)

The filter uses the black/whitelists from the configuration by default. You can also plug in you own IpChecker implementation. With runtime time DI you have to disable the default module in your application.conf and bind your implementation in your app's module:

play {
  modules {
    disabled += "com.digitaltangible.playguard.PlayGuardIpCheckerModule"
  }
}

2.2 Configuration

playguard {
  filter {
    enabled = true
    global {
      bucket {
        size = 100
        rate = 100
      }
    }
    ip {
      whitelist = ["1.1.1.1", "2.2.2.2"]
      blacklist = ["3.3.3.3", "4.4.4.4"]
      bucket {
        size = 50
        rate = 50
      }
    }
  }
}

3. Configuring the remote IP address

IP-based rate limits will use RequestHeader.remoteAddress as the address of the client. Depending on how you have configured Play this may be the actual remote address of clients connecting directly, or it may be read from the common X-Forwarded-For or Forwarded headers that are set by proxies and load balancers.

If you are using a reverse proxy (e.g. nginx, HAProxy or an AWS ELB) in front of your application, you should take care to configure the play.http.forwarded.trustedProxies setting, otherwise all requests will be rate-limited against the IP address of the upstream proxy (definitely not what you want).

For scenarios where you don't know your immediate connection's IP address beforehand (to configure it as a trusted proxy) but can still trust it, e.g. on Heroku, there is a custom RequestHandler XForwardedTrustImmediateConnectionRequestHandler which replaces the immediate connection with the last IP address in the X-Forwarded-For header (RFC 7239 is not supported). This handler can be configured as described here

play-guard's People

Contributors

arturalkaim avatar enalmada avatar jtjeferreira avatar sief avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

play-guard's Issues

Make TokenBucketGroup a trait to allow easier customization

If you create a trait:

trait TokenBucketGroup {
  def consume(key: Any, required: Int): Long
}

and rename the current class to DefaultTokenBucketGroup, then it would be easier to customize the TokenBucketGroup implementation - there are tradeoffs between exactness and computational complexity, the current implementation is exact but at the cost of doing up to rate bucket rebuilds per second, which is totally fine at 10/s, but less so at 10k/s.

I would also consider changing the return type from Long to Boolean since it's only (intended to be?) used in a boolean way - that would also hide more of the TokenBucketGroup internals.

(Yes, it's of course possible to just subclass and ignore the private members of the current TokenBucketGroup)

(Happy to provide a pull request, wanted to check for interest first)

Using Play Guard in HttpErrorHandler

Hi Simon,

My apologies for posting this here; this is not an issue but more of an implementation question. I am trying to implement the "HttpErrorRateLimitAction" on my auth controller, however if there is a body parsing error, the entire action is caught by the default http error handler and rate limiting will not work in this instance.

I have been trying to implement it in my custom "HttpErrorHandler" but I'm not quite sure as to where (or rather how) I should place the code.

def onClientError(request: RequestHeader, statusCode: Int, message: String) = {
    
    HttpErrorRateLimitAction(new RateLimiter(2, 1f / 10, "test failure rate limit")) {
      request => BadRequest("failure rate exceeded")
    }

    Logger.error("Client Error (" + statusCode + "):" + message)
    Future.successful(
      //Status(statusCode)("A client error occurred: " + message)
      BadRequest(Json.obj("status" -> Messages("unknown.error")))
    )
  } 

X-Forwarded-For parsing does not support more than one proxy, is a dangerous default

Currently, the last IP address in the X-Forwarded-For header is used by default as the originating IP address for the request:

} yield ip) getOrElse {
// Consider X-Forwarded-For as most accurate if it exists
// Since it is easy to forge an X-Forwarded-For, only consider the last ip added by our proxy as the most accurate
// https://en.wikipedia.org/wiki/X-Forwarded-For
request.headers.get("X-Forwarded-For").map(_.split(",").last.trim).getOrElse {
request.remoteAddress
}

There's quite a few problems with this approach:

  • If no IP is appended to the X-Forwarded-For header at all, the user can spoof their IP address to the rate limiter just by creating an X-Forwarded-For header of their own (effectively bypassing the limiter, or DoSing any IP address they like)
    • This will become more common as load-balancers move to RFC 7239 (Forwarded) headers, which aren't supported at all
  • It doesn't support configuration of trusted proxies (those that the determination of IP address should "look through")
  • etc.

I'd suggest that the default case in the getOrElse (L12) block should just be request.remoteAddress:

  • It can't be spoofed by the user, providing a much safer default
  • It provides compatibility with the built-in way of determining the actual IP address in situations where there's more than one proxy: Play's play.http.forwarded.trustedProxies setting
    • Which means it gives us RFC 7239 support "for free" too

I'm guessing play.http.forwarded.trustedProxies might not have existed when this code was conceived, but either way, request.remoteAddress is definitely all that needs to be considered now.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.