Skip to content
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

Closed
wants to merge 12 commits into from

Conversation

gjaldon
Copy link
Contributor

@gjaldon gjaldon commented Sep 13, 2014

Couldn't get the last 2 tests to pass. How to persist token in the session in the tests?


defp get_csrf_token(conn), do: Conn.get_session(conn, :csrf_token)

defp token_length, do: 32
Copy link
Member

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?

@chrismccord
Copy link
Member

@gjaldon Before running the tests you can just great a conn and generate a token for it before simulating the request. I'm not seeing any token generation/persistence code though? Once that's in place I can take a look at setting the test cases up

defp valid_authenticity_token?(conn, token), do: get_csrf_token(conn) == token


defp get_csrf_token(conn), do: Conn.get_session(conn, :csrf_token)
Copy link
Member

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).

@gjaldon
Copy link
Contributor Author

gjaldon commented Sep 14, 2014

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?

@josevalim
Copy link
Member

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.

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.

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.

You need to recycle the connection. Something like:

conn = conn(:get, ...)
conn = call(conn, ...)

conn = recycle(conn)
call(conn, ...)

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)
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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.

@gjaldon gjaldon changed the title [WIP] Csrf protection plug Csrf protection plug Sep 14, 2014
@gjaldon
Copy link
Contributor Author

gjaldon commented Sep 14, 2014

I updated all the specs now to use recycle/2 and provided 2 different ways to handle invalid_token that is configurable by setting the raise_error option. I also applied all the other suggestions and the docs should be updated now.

How do I test if a response was sent? Was it correct to use send_resp/3 in place of put_status in handle_invalid_token/1? Let me know if there's anything else I missed.

@gjaldon
Copy link
Contributor Author

gjaldon commented Sep 14, 2014

Closes #305

@gjaldon
Copy link
Contributor Author

gjaldon commented Sep 16, 2014

@chrismccord @josevalim this is ready for review, btw. 😃


## Examples

plug Phoenix.Plugs.CsrfProtection, raise_error: false
Copy link
Member

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.

@gjaldon
Copy link
Contributor Author

gjaldon commented Sep 16, 2014

@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([])
Copy link
Member

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

Copy link
Contributor Author

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!

@chrismccord
Copy link
Member

@gjaldon Can you rebase off master? We have ConnHelper now. RouteHelper no longer exists. Once we get the conflicts cleared up this should be just about ready to go.

@josevalim
Copy link
Member

I would honestly define a new exception, raise it on the plug, implement Plug.Exception for it, and call it a day.

@josevalim josevalim closed this Sep 24, 2014
@josevalim josevalim reopened this Sep 24, 2014
@chrismccord
Copy link
Member

I would honestly define a new exception, raise it on the plug, implement Plug.Exception for it, and call it a day.

👍

@gjaldon
Copy link
Contributor Author

gjaldon commented Sep 25, 2014

I applied all of @josevalim's suggestions, updated the tests and rebased to master. Let me know if there's anything I missed 😄

@josevalim
Copy link
Member

I plan to review this soonish.

@chrismccord chrismccord mentioned this pull request Nov 19, 2014
@josevalim
Copy link
Member

Can we merge this after 0.6.0? I am considering adding this to plug but I need more time. :S

@chrismccord
Copy link
Member

Yep. It can wait. Worst case we can release a 6.x with it.

@stevedomin
Copy link
Contributor

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") ?

@chrismccord
Copy link
Member

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.

@stevedomin
Copy link
Contributor

That makes sense, thanks Chris.

@josevalim
Copy link
Member

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)
Copy link
Member

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.

@josevalim
Copy link
Member

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]
Copy link
Member

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.

Copy link
Member

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. :)

@josevalim
Copy link
Member

Two more notes:

  1. It would be nice if we have a mechanism to skip the CSRF token if something is set inside the conn.private. For that, we could check if conn.private[:plug_skip_csrf_protection] is set to true and skip it. This would be useful for testing.
  2. We should also add protection to this style of cross requests: rails/rails@1650bb3

Feel free to ping me if you need more info!

@gjaldon
Copy link
Contributor Author

gjaldon commented Dec 2, 2014

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. :)

@gjaldon gjaldon mentioned this pull request Dec 5, 2014
@gjaldon
Copy link
Contributor Author

gjaldon commented Dec 10, 2014

Closing in favor of adding CSRF to Plug and #523

@gjaldon gjaldon closed this Dec 10, 2014
@gjaldon gjaldon deleted the csrf-protection-plug branch December 10, 2014 13:37
Gazler pushed a commit to Gazler/phoenix that referenced this pull request Sep 17, 2015
Mention Phoenix.Socket.assign/3 in the channel guide
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants