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