I'm working to address CSRF vulnerabilities in my React/Phoenix app, and it seems to me like my app is safe... but I'm not an expert in these matters, and wanted to turn to the community to see if I've overlooked something or am being naïve.
The Phoenix is a pure API, running separately from the React client, so I'm dealing with CORS - the whitelist of allowed origins is set in the Phoenix router.ex
:
pipeline :api do
plug CORSPlug, [origin: "localhost:3000"]
plug :accepts, ["json"]
plug Guardian.Plug.VerifyHeader, realm: "Bearer"
plug Guardian.Plug.LoadResource
end
and, as you can see, I'm using Guardian (which uses JWT for user authentication) for handling authorization.
Authorized clients store the JWT in localStorage
, and Guardian is set to look for that value in the Authorization
header of requests as a Bearer
... protected Phoenix controllers include:
plug Guardian.Plug.EnsureAuthenticated
I set up a test attacker, running on localhost:5000
to try to mock a CSRF attack. First, I tried an AJAX attack - I copied a valid JWT value from the localStorage
of a logged-in window and set it in the request header of my mock attacker. As expected, this fails because localhost:5000
is not whitelisted
The 'Access-Control-Allow-Origin' header contains the invalid value 'null'. Origin 'http://localhost:5000' is therefore not allowed access.
To test, I added localhost:5000
to the Phoenix whitelist and the request did indeed work... so it seems that, even if an attacker managed to steal a valid JWT, they'd be stopped by the whitelist.
I then tested an automated form submission, borrowed from the OWASP docs:
<body onload='document.CSRF.submit()'>
<form action='http://localhost:4000/api/v1/user' method='POST' name='CSRF'>
<input type='hidden' name='name' value='Hacked'>
<input type='hidden' name='password' value='Hacked'>
</form>
<body>
but this gets caught by Guardian.Plug.EnsureAuthenticated
, set in the API's controllers, since there is no Authorization
header nor valid JWT present:
[info] POST /api/v1/user
[debug] Processing with MyApp.UserController.create/2
Parameters: %{"name" => "Hacked", "password" => "[FILTERED]"}
Pipelines: [:api]
[info] Sent 401 in 21ms
[debug] MyApp.UserController halted in
Guardian.Plug.EnsureAuthenticated.call/2
So my impression is that AJAX attacks will fail, even with a valid JWT, because of the CORS whitelist... simple requests will fail because they don't include the Authorization
header.
I've been reading a lot about CSRF protection when using JWTs for authorization, but it seems like no two people can agree on what is and isn't safe. Am I missing something, or is the combination of the CORS whitelist and Guardian JWT check sufficient to protect agains CSRF?
You mention 2 methods for CSRF prevention: 1) CORS & 2) JWT stored in localStorage and associated to the user's session. I would like to address both:
1) CORS does help prevent certain types of CSRF attacks by preventing attempts from non-origin sources from making HTTP requests on the user's behalf. This prevents GETs/POSTs from external sources, good job. It will not prevent CSRF attacks from internal sources. So on to #2...
2) JWT stored in localStorage passed thru Authentication/Bearer header can help but is always still susceptible to XSS attacks. XSS prevention is critical. Now the JWT is accessible via javascript and can be passed with any request. To help, the website should prevent javascript access to the token. The recommended approach is to store the JWT in an HTTPOnly cookie. This cookie is in addition to the Authentication/Bearer header. On the Phoenix server side, the first authorization step the API needs to do is ensure that the JWT from the HTTPOnly cookie is the same as the Authentication/Bearer header. Only then can the API be successfully invoked.