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?