-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Csrf protection plug #338
Csrf protection plug #338
Conversation
|
||
defp get_csrf_token(conn), do: Conn.get_session(conn, :csrf_token) | ||
|
||
defp token_length, do: 32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it's unused? Where do we persist the token in the session? I'm not seeing token generation/persistence anywhere?
@gjaldon Before running the tests you can just great a |
defp valid_authenticity_token?(conn, token), do: get_csrf_token(conn) == token | ||
|
||
|
||
defp get_csrf_token(conn), do: Conn.get_session(conn, :csrf_token) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is assuming the session was previously fetched. Is this assumption ok? Should we fetch automatically or should we document the user must do so? Given Phoenix fetches the session automatically, I would go with assume the session was previously fetched and explicitly document it (in the moduledoc).
f816f79
to
7a792dc
Compare
I've applied most of the changes suggested. I have yet to look into how to handle a request with an invalid authenticity token though(still have to look up why Rails makes this configurable). I generate and persist the token in the session if it does not previously exist here but just realized this won't work since all first requests will always be unprotected. I'm thinking of replacing it with a function like: def csrf_token_param(conn) do
if token = get_csrf_token(conn) do
token
else
token = generate_csrf_token(token_length)
store.put(sid, conn.private[:plug_session], store_config)
end
end This will be called in the View/Template so a csrf_token is always generated. In my tests, the session doesn't persist across multiple requests. It looks like it's because I'm using cookie store which requires a browser. I'm thinking of using a different store in the test. Will that be okay? |
That's exactly how CSRF is supposed to work. The first request is usually a GET that sets up the token, we should not set the token or generate one in the same request as it would lead all blank requests (with no token) to pass.
You need to recycle the connection. Something like:
The recycle will move the cookies set in the response to a cookie to be sent as request (as a browser would). |
def init(opts), do: opts | ||
|
||
def call(%Conn{method: method} = conn, _opts) when method in @protected_methods do | ||
conn = ensure_csrf_token(conn) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we generate it here, it will necessarily be verified no matter what. So we'll need to expose the generate so it's call callable from elsewhere, i.e. the Views as you've discussed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is very hard to do in the views because the views would now have to return a new connection. In general I would argue for it to be generated before hand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably ensure it exists only on GET requests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right, good point. We have to do it before. Yeah I think generate it on get requests in the non @protected_methods
clause would be fine.
I updated all the specs now to use How do I test if a response was sent? Was it correct to use |
Closes #305 |
@chrismccord @josevalim this is ready for review, btw. 😃 |
|
||
## Examples | ||
|
||
plug Phoenix.Plugs.CsrfProtection, raise_error: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't raise an error. Instead we should halt the connection or clear the session and continue. Maybe halt: true
? Personally, I'm for always halting until someone needs/wants the ability for the stack to continue. The safest approach is to halt always. So maybe we do away with an option entirely for now.
@chrismccord thanks for the inputs! Should be ready for review again. |
end | ||
|
||
test "unprotected requests are always valid" do | ||
simulate_request_without_token(:get, "/") |> CsrfProtection.call([]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need asserts for all these
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chrismccord Done adding all the assertions!
@gjaldon Can you rebase off master? We have |
I would honestly define a new exception, raise it on the plug, implement Plug.Exception for it, and call it a day. |
👍 |
7d3f356
to
f8e4293
Compare
I applied all of @josevalim's suggestions, updated the tests and rebased to master. Let me know if there's anything I missed 😄 |
I plan to review this soonish. |
Can we merge this after 0.6.0? I am considering adding this to plug but I need more time. :S |
Yep. It can wait. Worst case we can release a 6.x with it. |
e62c2a2
to
168abe9
Compare
Hi guys, Since the goal of plug is to create composable modules that can be used by different web apps shouldn't this stuff be extracted from phoenix and be put either in a separate repo or in the plug repo itself maybe (csrf protection is quite important to any website really so I definitely think it can be part of the "essentials") ? |
Plug definitely lets the community easily share composable modules. Our approach right now is to implement what we need in Phoenix, then extract if/when it makes sense. So far, the extracted stuff has made its way back into Plug, but in the future it's possible we'll release standalone projects. It's not a priority right now though. Up through 1.0, we won't be extracting much as it will slow us down since APIs are subject to change. |
That makes sense, thanks Chris. |
Just want to say sorry for the delay on this @gjaldon. It is on my plate to review this and the forms work next. |
plug Phoenix.Plugs.CsrfProtection | ||
|
||
""" | ||
@protected_methods ~w(POST PUT PATCH DELETE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of checking for those, let's protect for ALL except HEAD and GET.
I have reviewed and this looks great. Can you please submit this pull request in the Plug repo and I will get to merge it asap. :) |
Error raised when CSRF token is invalid. | ||
""" | ||
|
||
defexception [:message] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In latest Plug you don't need to implement Plug.Exception, just add a field called plug_status: 403
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should also specify the default error message here, instead of doing it when raising. :)
Two more notes:
Feel free to ping me if you need more info! |
No problem about the delay, @josevalim! Will apply your suggestions and submit the PR in the Plug repo. Will likely finish this over the next few days. :) |
Closing in favor of adding CSRF to Plug and #523 |
Mention Phoenix.Socket.assign/3 in the channel guide
Couldn't get the last 2 tests to pass. How to persist token in the session in the tests?