zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

csrf_protection.ex (15011B)


      1 defmodule Plug.CSRFProtection do
      2   @moduledoc """
      3   Plug to protect from cross-site request forgery.
      4 
      5   For this plug to work, it expects a session to have been
      6   previously fetched. It will then compare the token stored
      7   in the session with the one sent by the request to determine
      8   the validity of the request. For an invalid request the action
      9   taken is based on the `:with` option.
     10 
     11   The token may be sent by the request either via the params
     12   with key "_csrf_token" or a header with name "x-csrf-token".
     13 
     14   GET requests are not protected, as they should not have any
     15   side-effect or change your application state. JavaScript
     16   requests are an exception: by using a script tag, external
     17   websites can embed server-side generated JavaScript, which
     18   can leak information. For this reason, this plug also forbids
     19   any GET JavaScript request that is not XHR (or AJAX).
     20 
     21   Note that it is recommended to enable CSRFProtection whenever
     22   a session is used, even for JSON requests. For example, Chrome
     23   had a bug that allowed POST requests to be triggered with
     24   arbitrary content-type, making JSON exploitable. More info:
     25   https://bugs.chromium.org/p/chromium/issues/detail?id=490015
     26 
     27   Finally, we recommend developers to invoke `delete_csrf_token/0`
     28   every time after they log a user in, to avoid CSRF fixation
     29   attacks.
     30 
     31   ## Token generation
     32 
     33   This plug won't generate tokens automatically. Instead, tokens
     34   will be generated only when required by calling `get_csrf_token/0`.
     35   In case you are generating the token for certain specific URL,
     36   you should use `get_csrf_token_for/1` as that will avoid tokens
     37   from being leaked to other applications.
     38 
     39   Once a token is generated, it is cached in the process dictionary.
     40   The CSRF token is usually generated inside forms which may be
     41   isolated from `Plug.Conn`. Storing them in the process dictionary
     42   allows them to be generated as a side-effect only when necessary,
     43   becoming one of those rare situations where using the process
     44   dictionary is useful.
     45 
     46   ## Cross-host protection
     47 
     48   If you are sending data to a full URI, such as `//subdomain.host.com/path`
     49   or `//external.com/path`, instead of a simple path such as `/path`, you may
     50   want to consider using `get_csrf_token_for/1`, as that will encode the host
     51   in the CSRF token. Once received, Plug will only consider the CSRF token to
     52   be valid if the `host` encoded in the token is the same as the one in
     53   `conn.host`.
     54 
     55   Therefore, if you get a warning that the host does not match, it is either
     56   because someone is attempting to steal CSRF tokens or because you have a
     57   misconfigured host configuration.
     58 
     59   For example, if you are running your application behind a proxy, the browser
     60   will send a request to the proxy with `www.example.com` but the proxy will
     61   request you using an internal IP. In such cases, it is common for proxies
     62   to attach information such as `"x-forwarded-host"` that contains the original
     63   host.
     64 
     65   This may also happen on redirects. If you have a POST request to `foo.example.com`
     66   that redirects to `bar.example.com` with status 307, the token will contain a
     67   different host than the one in the request.
     68 
     69   You can pass the `:allow_hosts` option to control any host that you may want
     70   to allow. The values in `:allow_hosts` may either be a full host name or a
     71   host suffix. For example: `["www.example.com", ".subdomain.example.com"]`
     72   will allow the exact host of `"www.example.com"` and any host that ends with
     73   `".subdomain.example.com"`.
     74 
     75   ## Options
     76 
     77     * `:session_key` - the name of the key in session to store the token under
     78     * `:allow_hosts` - a list with hosts to allow on cross-host tokens
     79     * `:with` - should be one of `:exception` or `:clear_session`. Defaults to
     80     `:exception`.
     81       * `:exception` -  for invalid requests, this plug will raise
     82       `Plug.CSRFProtection.InvalidCSRFTokenError`.
     83       * `:clear_session` -  for invalid requests, this plug will set an empty
     84       session for only this request. Also any changes to the session during this
     85       request will be ignored.
     86 
     87   ## Disabling
     88 
     89   You may disable this plug by doing
     90   `Plug.Conn.put_private(conn, :plug_skip_csrf_protection, true)`. This was made
     91   available for disabling `Plug.CSRFProtection` in tests and not for dynamically
     92   skipping `Plug.CSRFProtection` in production code. If you want specific routes to
     93   skip `Plug.CSRFProtection`, then use a different stack of plugs for that route that
     94   does not include `Plug.CSRFProtection`.
     95 
     96   ## Examples
     97 
     98       plug Plug.Session, ...
     99       plug :fetch_session
    100       plug Plug.CSRFProtection
    101 
    102   """
    103 
    104   import Plug.Conn
    105   require Bitwise
    106   require Logger
    107 
    108   alias Plug.Crypto.KeyGenerator
    109   alias Plug.Crypto.MessageVerifier
    110 
    111   @behaviour Plug
    112   @unprotected_methods ~w(HEAD GET OPTIONS)
    113   @digest Base.url_encode64("HS256", padding: false) <> "."
    114 
    115   # The token size value should not generate padding
    116   @token_size 18
    117   @encoded_token_size 24
    118   @double_encoded_token_size 32
    119 
    120   defmodule InvalidCSRFTokenError do
    121     @moduledoc "Error raised when CSRF token is invalid."
    122 
    123     message =
    124       "invalid CSRF (Cross Site Request Forgery) token, please make sure that:\n\n" <>
    125         "  * The session cookie is being sent and session is loaded\n" <>
    126         "  * The request include a valid '_csrf_token' param or 'x-csrf-token' header"
    127 
    128     defexception message: message, plug_status: 403
    129   end
    130 
    131   defmodule InvalidCrossOriginRequestError do
    132     @moduledoc "Error raised when non-XHR requests are used for Javascript responses."
    133 
    134     message =
    135       "security warning: an embedded <script> tag on another site requested " <>
    136         "protected JavaScript (if you know what you're doing, disable forgery " <>
    137         "protection for this route)"
    138 
    139     defexception message: message, plug_status: 403
    140   end
    141 
    142   ## API
    143 
    144   @doc """
    145   Load CSRF state into the process dictionary.
    146 
    147   This can be used to load CSRF state into another process.
    148   See `dump_state/0` and `dump_state_from_session/2` for dumping it.
    149 
    150   ## Examples
    151 
    152   To dump the state from the current process and load into another one:
    153 
    154       csrf_state = Plug.CSRFProtection.dump_state()
    155       secret_key_base = conn.secret_key_base
    156 
    157       Task.async(fn ->
    158         Plug.CSRFProtection.load_state(secret_key_base, csrf_state)
    159       end)
    160 
    161   If you have a session but the CSRF state was not loaded into the
    162   current process, you can dump the state from the session:
    163 
    164       csrf_state = Plug.CSRFProtection.dump_state_from_session(session["_csrf_token"])
    165 
    166       Task.async(fn ->
    167         Plug.CSRFProtection.load_state(secret_key_base, csrf_state)
    168       end)
    169 
    170   """
    171   def load_state(secret_key_base, csrf_state) when is_binary(csrf_state) or is_nil(csrf_state) do
    172     Process.put(:plug_unmasked_csrf_token, csrf_state)
    173     Process.put(:plug_csrf_token_per_host, %{secret_key_base: secret_key_base})
    174     :ok
    175   end
    176 
    177   @doc """
    178   Dump CSRF state from the process dictionary.
    179 
    180   This allows it to be loaded in another process.
    181 
    182   See `load_state/2` for more information.
    183   """
    184   def dump_state() do
    185     unmasked_csrf_token()
    186   end
    187 
    188   @doc """
    189   Dumps the CSRF state from the session token.
    190 
    191   It expects the value of `get_session(conn, "_csrf_token")`
    192   as input. It returns `nil` if the given token is not valid.
    193   """
    194   def dump_state_from_session(session_token) do
    195     if is_binary(session_token) and byte_size(session_token) == @encoded_token_size do
    196       session_token
    197     end
    198   end
    199 
    200   @doc """
    201   Validates the `csrf_token` against the state.
    202 
    203   This is the mechanism used by the Plug itself to match the token
    204   received in the request (via headers or parameters) with the state
    205   (typically stored in the session).
    206   """
    207   def valid_state_and_csrf_token?(state, csrf_token) do
    208     with <<state_token::@encoded_token_size-binary>> <-
    209            state,
    210          <<user_token::@double_encoded_token_size-binary, mask::@encoded_token_size-binary>> <-
    211            csrf_token do
    212       valid_masked_token?(state_token, user_token, mask)
    213     else
    214       _ -> false
    215     end
    216   end
    217 
    218   @doc """
    219   Gets the CSRF token.
    220 
    221   Generates a token and stores it in the process
    222   dictionary if one does not exist.
    223   """
    224   def get_csrf_token do
    225     if token = Process.get(:plug_masked_csrf_token) do
    226       token
    227     else
    228       token = mask(unmasked_csrf_token())
    229       Process.put(:plug_masked_csrf_token, token)
    230       token
    231     end
    232   end
    233 
    234   @doc """
    235   Gets the CSRF token for the associated URL (as a string or a URI struct).
    236 
    237   If the URL has a host, a CSRF token that is tied to that
    238   host will be generated. If it is a relative path URL, a
    239   simple token emitted with `get_csrf_token/0` will be used.
    240   """
    241   def get_csrf_token_for(url) when is_binary(url) do
    242     case url do
    243       <<"/">> -> get_csrf_token()
    244       <<"/", not_slash, _::binary>> when not_slash != ?/ -> get_csrf_token()
    245       _ -> get_csrf_token_for(URI.parse(url))
    246     end
    247   end
    248 
    249   def get_csrf_token_for(%URI{host: nil}) do
    250     get_csrf_token()
    251   end
    252 
    253   def get_csrf_token_for(%URI{host: host}) do
    254     case Process.get(:plug_csrf_token_per_host) do
    255       %{^host => token} ->
    256         token
    257 
    258       %{secret_key_base: secret} = secrets ->
    259         unmasked = unmasked_csrf_token()
    260         message = generate_token() <> host
    261         key = KeyGenerator.generate(secret, unmasked)
    262         token = MessageVerifier.sign(message, key)
    263         Process.put(:plug_csrf_token_per_host, Map.put(secrets, host, token))
    264         token
    265 
    266       _ ->
    267         raise "cannot generate CSRF token for a host because get_csrf_token_for/1 is invoked " <>
    268                 "in a separate process than the one that started the request"
    269     end
    270   end
    271 
    272   @doc """
    273   Deletes the CSRF token from the process dictionary.
    274 
    275   This will force the token to be deleted once the response is sent.
    276   If you want to refresh the CSRF state, you can call `get_csrf_token/0`
    277   after `delete_csrf_token/0` to ensure a new token is generated.
    278   """
    279   def delete_csrf_token do
    280     case Process.get(:plug_csrf_token_per_host) do
    281       %{secret_key_base: secret_key_base} ->
    282         Process.put(:plug_csrf_token_per_host, %{secret_key_base: secret_key_base})
    283         Process.put(:plug_unmasked_csrf_token, :delete)
    284 
    285       _ ->
    286         :ok
    287     end
    288 
    289     Process.delete(:plug_masked_csrf_token)
    290   end
    291 
    292   ## Plug
    293 
    294   @impl true
    295   def init(opts) do
    296     session_key = Keyword.get(opts, :session_key, "_csrf_token")
    297     mode = Keyword.get(opts, :with, :exception)
    298     allow_hosts = Keyword.get(opts, :allow_hosts, [])
    299     {session_key, mode, allow_hosts}
    300   end
    301 
    302   @impl true
    303   def call(conn, {session_key, mode, allow_hosts}) do
    304     csrf_token = dump_state_from_session(get_session(conn, session_key))
    305     load_state(conn.secret_key_base, csrf_token)
    306 
    307     conn =
    308       cond do
    309         verified_request?(conn, csrf_token, allow_hosts) ->
    310           conn
    311 
    312         mode == :clear_session ->
    313           conn |> configure_session(ignore: true) |> clear_session()
    314 
    315         mode == :exception ->
    316           raise InvalidCSRFTokenError
    317 
    318         true ->
    319           raise ArgumentError,
    320                 "option :with should be one of :exception or :clear_session, got #{inspect(mode)}"
    321       end
    322 
    323     register_before_send(conn, &ensure_same_origin_and_csrf_token!(&1, session_key, csrf_token))
    324   end
    325 
    326   ## Verification
    327 
    328   defp verified_request?(conn, csrf_token, allow_hosts) do
    329     conn.method in @unprotected_methods ||
    330       valid_csrf_token?(conn, csrf_token, body_csrf_token(conn), allow_hosts) ||
    331       valid_csrf_token?(conn, csrf_token, header_csrf_token(conn), allow_hosts) ||
    332       skip_csrf_protection?(conn)
    333   end
    334 
    335   defp header_csrf_token(conn), do: List.first(get_req_header(conn, "x-csrf-token"))
    336 
    337   defp body_csrf_token(%{body_params: %{"_csrf_token" => csrf_token}}), do: csrf_token
    338   defp body_csrf_token(_), do: nil
    339 
    340   defp valid_csrf_token?(
    341          _conn,
    342          <<csrf_token::@encoded_token_size-binary>>,
    343          <<user_token::@double_encoded_token_size-binary, mask::@encoded_token_size-binary>>,
    344          _allow_hosts
    345        ) do
    346     valid_masked_token?(csrf_token, user_token, mask)
    347   end
    348 
    349   defp valid_csrf_token?(
    350          conn,
    351          <<csrf_token::@encoded_token_size-binary>>,
    352          <<@digest, _::binary>> = signed_user_token,
    353          allow_hosts
    354        ) do
    355     key = KeyGenerator.generate(conn.secret_key_base, csrf_token)
    356 
    357     case MessageVerifier.verify(signed_user_token, key) do
    358       {:ok, <<_::@encoded_token_size-binary, host::binary>>} ->
    359         if host == conn.host or Enum.any?(allow_hosts, &allowed_host?(&1, host)) do
    360           true
    361         else
    362           Logger.error("""
    363           Plug.CSRFProtection generated token for host #{inspect(host)} \
    364           but the host for the current request is #{inspect(conn.host)}. \
    365           See Plug.CSRFProtection documentation for more information.
    366           """)
    367 
    368           false
    369         end
    370 
    371       :error ->
    372         false
    373     end
    374   end
    375 
    376   defp valid_csrf_token?(_conn, _csrf_token, _user_token, _allowed_host), do: false
    377 
    378   defp valid_masked_token?(csrf_token, user_token, mask) do
    379     case Base.url_decode64(user_token) do
    380       {:ok, user_token} -> Plug.Crypto.masked_compare(csrf_token, user_token, mask)
    381       :error -> false
    382     end
    383   end
    384 
    385   defp allowed_host?("." <> _ = allowed, host), do: String.ends_with?(host, allowed)
    386   defp allowed_host?(allowed, host), do: allowed == host
    387 
    388   ## Before send
    389 
    390   defp ensure_same_origin_and_csrf_token!(conn, session_key, csrf_token) do
    391     if cross_origin_js?(conn) do
    392       raise InvalidCrossOriginRequestError
    393     end
    394 
    395     ensure_csrf_token(conn, session_key, csrf_token)
    396   end
    397 
    398   defp cross_origin_js?(%Plug.Conn{method: "GET"} = conn),
    399     do: not skip_csrf_protection?(conn) and not xhr?(conn) and js_content_type?(conn)
    400 
    401   defp cross_origin_js?(%Plug.Conn{}), do: false
    402 
    403   defp js_content_type?(conn) do
    404     conn
    405     |> get_resp_header("content-type")
    406     |> Enum.any?(&String.starts_with?(&1, ~w(text/javascript application/javascript)))
    407   end
    408 
    409   defp xhr?(conn) do
    410     "XMLHttpRequest" in get_req_header(conn, "x-requested-with")
    411   end
    412 
    413   defp ensure_csrf_token(conn, session_key, csrf_token) do
    414     Process.delete(:plug_masked_csrf_token)
    415 
    416     case Process.delete(:plug_unmasked_csrf_token) do
    417       ^csrf_token -> conn
    418       nil -> conn
    419       :delete -> delete_session(conn, session_key)
    420       current -> put_session(conn, session_key, current)
    421     end
    422   end
    423 
    424   ## Helpers
    425 
    426   defp skip_csrf_protection?(%Plug.Conn{private: %{plug_skip_csrf_protection: true}}), do: true
    427   defp skip_csrf_protection?(%Plug.Conn{}), do: false
    428 
    429   defp mask(token) do
    430     mask = generate_token()
    431     Base.url_encode64(Plug.Crypto.mask(token, mask)) <> mask
    432   end
    433 
    434   defp unmasked_csrf_token do
    435     case Process.get(:plug_unmasked_csrf_token) do
    436       token when is_binary(token) ->
    437         token
    438 
    439       _ ->
    440         token = generate_token()
    441         Process.put(:plug_unmasked_csrf_token, token)
    442         token
    443     end
    444   end
    445 
    446   defp generate_token do
    447     Base.url_encode64(:crypto.strong_rand_bytes(@token_size))
    448   end
    449 end