PlugAttack

repository·main·Indexed 19 days ago

https://github.com/michalmuskala/plug_attack

A toolkit for Elixir/Phoenix applications to block and throttle abusive requests using Plug. It provides a DSL for defining safelists, blocklists, and throttling logic via the rule/2 macro, including support for fail2ban and ETS-backed storage for tracking request limits.

Tokens
3.4K
Snippets
16
Records
18
Agent score
66%

What's inside plug_attack

  1. Use throttling data to set rate limit headers

    main

    When a rule triggers a throttle, the data map contains useful information for generating HTTP headers. Specifically:

    • data[:limit]: The maximum allowed requests.
    • data[:remaining]: The number of requests remaining in the current window.
    • data[:expires_at]: The expiration time in Unix time milliseconds. To use this in the standard X-Ratelimit-Reset header, you must convert it to seconds by dividing by 1,000.
    defp add_throttling_headers(conn, data) do
      # Convert milliseconds to seconds for the reset header
      reset = div(data[:expires_at], 1_000)
      conn
      |> put_resp_header("x-ratelimit-limit", to_string(data[:limit]))
      |> put_resp_header("x-ratelimit-remaining", to_string(data[:remaining]))
      |> put_resp_header("x-ratelimit-reset", to_string(reset))
    end
  2. Configure ETS storage for throttling

    main

    Throttling requires a storage backend to be added to your application's supervision tree. PlugAttack.Storage.Ets is a common choice. You can configure a clean_period (in milliseconds) to manage memory by removing stale data.

    children = [
      {PlugAttack.Storage.Ets, name: MyApp.PlugAttack.Storage, clean_period: 60_000}
    ]
  3. Define basic rules with PlugAttack

    main

    To create a custom attack plug, use the PlugAttack module. You define rules using the rule/2 macro, which provides a connection (conn) and allows you to specify conditions for allowing or blocking requests using the allow/1 function.

    Note on Proxies: If your application is behind a proxy (e.g., Nginx or Heroku), ensure you use a plug like remote_ip to correctly populate conn.remote_ip from X-Forwarded-For headers before using plug_attack.

    defmodule MyApp.PlugAttack do
      use PlugAttack
    
      rule "allow local", conn do
        allow conn.remote_ip == {127, 0, 0, 1}
      end
    end
  4. Define request rules with PlugAttack.Rule

    main
    The PlugAttack.Rule module provides a DSL for defining security rules used within the PlugAttack.rule/2 macro. Rules determine whether a request is allowed, blocked, throttled, or banned. If a rule's condition is not met (e.g., a key is falsey), the rule returns nil, and the next rule in the sequence is evaluated.
  5. Set up PlugAttack.Storage.Ets in your supervision tree

    main

    PlugAttack.Storage.Ets is a storage implementation that uses a local ETS table to track throttling and fail2ban state. To use it, you must add it to your application's supervision tree.

    When starting the process, you provide a unique name (an atom) which will be used to identify the ETS table and the registered process. You can also configure the :clean_period to control how often stale data is removed from memory.

    Once started, you can pass this storage to various PlugAttack rules using the :storage option in the format: storage: {PlugAttack.Storage.Ets, YourStorageName}.

    children = [
      # ...
      {PlugAttack.Storage.Ets, [MyApp.PlugAttackStorage]}
    ]
  6. Integrate PlugAttack into your application

    main

    To use PlugAttack, include use PlugAttack in your module. This automatically implements the Plug behaviour and provides the rule/2 and rule/3 macros. You can also override the default behaviors for when a request is allowed or blocked by defining allow_action/3 and block_action/3.

    defmodule MyApp.PlugAttack do
      import Plug.Conn
      use PlugAttack
    
      # Define rules here...
    
      # Optional: Customize what happens when a request is allowed
      def allow_action(conn, _data, _opts), do: conn
    
      # Optional: Customize what happens when a request is blocked
      def block_action(conn, _data, _opts) do
        conn
        |> send_resp(:forbidden, "Forbidden\n")
        |> halt
      end
    end
  7. Customize allow and block actions

    main

    You can intercept the results of your rules by defining allow_action/3 and block_action/3 in your plug module.

    • allow_action(conn, data, opts): Called when a rule allows a request. If the rule was a throttle, data will be {:throttle, data_map}.
    • block_action(conn, data, opts): Called when a rule blocks a request. If the rule was a throttle, data will be {:throttle, data_map}.

    When blocking, it is important to halt the connection after sending a response to prevent further processing.

    # Example implementation of custom actions
    def allow_action(conn, {:throttle, data}, opts) do
      conn
      |> add_throttling_headers(data)
      |> allow_action(true, opts)
    end
    
    def allow_action(conn, _data, _opts) do
      conn
    end
    
    def block_action(conn, {:throttle, data}, opts) do
      conn
      |> add_throttling_headers(data)
      |> block_action(false, opts)
    end
    
    def block_action(conn, _data, _opts) do
      conn
      |> send_resp(:forbidden, "Forbidden\n")
      |> halt
    end
  8. Implement request throttling

    main

    To throttle requests, use the throttle/2 function within a rule. You must provide a storage backend (typically the ETS storage configured in your supervision tree).

    Parameters for throttle/2:

    • key: The identifier for the throttle (e.g., conn.remote_ip).
    • period: The time window in milliseconds.
    • limit: The maximum number of requests allowed within the period.
    • storage: A tuple containing the storage module and its name, e.g., {PlugAttack.Storage.Ets, MyApp.PlugAttack.Storage}.
    rule "throttle by ip", conn do
      throttle conn.remote_ip,
        period: 60_000, limit: 10,
        storage: {PlugAttack.Storage.Ets, MyApp.PlugAttack.Storage}
    end
  9. Configure the clean_period for PlugAttack.Storage.Ets

    main

    When calling PlugAttack.Storage.Ets.start_link/2, you can provide an options list to configure the cleanup frequency.

    • :clean_period - The interval (in milliseconds) at which the ETS table is cleaned of stale data to limit memory consumption. Defaults to 5000 ms.
    PlugAttack.Storage.Ets.start_link(MyApp.PlugAttackStorage, [clean_period: 10_000])
  10. Throttle requests using the throttle/2 helper

    main

    The throttle/2 helper (available inside rule blocks) allows you to limit requests based on a key (e.g., IP address or user email).

    Arguments:

    • key: The value used to identify the client (e.g., conn.remote_ip).
    • opts: An options map containing:
      • period: The time window in milliseconds.
      • limit: The maximum number of requests allowed within the period.
      • storage: A tuple {StorageModule, StorageName}. By default, an :ets backed implementation is used.

    If the throttle limit is exceeded, the rule returns {:block, data}, triggering block_action/3.

    rule "throttle per ip", conn do
      # throttle to 5 requests per second
      throttle conn.remote_ip,
        period: 1_000, limit: 5,
        storage: {PlugAttack.Storage.Ets, MyApp.PlugAttack.Storage}
    end
    
    rule "throttle login requests", conn do
      if conn.method == "POST" and conn.path_info == ["login"] do
        throttle conn.params["email"],
          period: 60_000, limit: 10,
          storage: {PlugAttack.Storage.Ets, MyApp.PlugAttack.Storage}
      end
    end