-
Notifications
You must be signed in to change notification settings - Fork 6.9k
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
[MM-10346] CSRF Token Implementation for Plugins #9192
Conversation
f426725
to
fc53647
Compare
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.
Looks good! Just a couple comments.
app/plugin_api.go
Outdated
return nil, err | ||
} | ||
|
||
session.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.
Lets give the plugins access to the session 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.
Done.
app/plugin_requests.go
Outdated
session, err := a.GetSession(token) | ||
csrfCheck := true | ||
|
||
if err == nil && r.Method != "GET" && r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML { |
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 check the header for GET requests as well on the server. Let's match that behavior. (But don't check the token for 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.
Never mind this is handled upstream.
fc53647
to
24cd0fe
Compare
Thanks for the review @crspeller! |
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.
Nice work, @DSchalla! A few comments below.
Also, @jwilander and @crspeller, it worries me that our Props
fields have fixed lengths: 1000
in this case of the Session
object. Have we run into issues before where new props (+ encoding overhead) overran this buffer size? Perhaps without being directly detected?
model/session.go
Outdated
return "" | ||
} | ||
|
||
val, ok := me.Props["csrf"] |
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.
Props
appears to be a map[string]string
, so you can just return me.Props["csrf"]
directly here and rely on the nil
value of ""
.
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.
Fixed - ty!
model/session_test.go
Outdated
token := s.GetCSRF() | ||
|
||
if token != "" { | ||
t.Errorf("Expected empty, got %s", 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.
nit: Might consider just using assert.Empty
and assert.NotEqual
below for simplicity.
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.
Good call, thanks!
@@ -126,7 +126,7 @@ func (a *App) GetUserForLogin(id, loginId string) (*model.User, *model.AppError) | |||
|
|||
func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) (*model.Session, *model.AppError) { | |||
session := &model.Session{UserId: user.Id, Roles: user.GetRawRoles(), DeviceId: deviceId, IsOAuth: false} | |||
|
|||
session.GenerateCSRF() |
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.
How do we decide whether or not to generate a CSRF token? For example, there's createSessionForUserAccessToken and oAuth's newSession -- should we be generating tokens in those cases as well?
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.
Good call, I added it for now to oauth and user access tokens as well to have all cases covered. For user access tokens it could be discussed for sure.
plugin/api.go
Outdated
// GetSession returns the session object for the Session ID | ||
GetSession(sessionId string) (*model.Session, *model.AppError) | ||
|
||
// GetCSRF returns the CSRF token for the session Id |
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.
Is GetCSRF
strictly required, given one can call GetCSRF
on the result of GetSession
above? Hoping we can keep the plugin API lean, and introduce helpers as needed (e.g. Session.GetCSRF
could check if the pointer receiver is nil
and return ""
directly).
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.
The idea was to provide a as convenient integration as possible for the plugin developer, it's certainly not much more than a helper. That way you can get the CSRF token directly via the session id without fetching the session object itself.
@@ -7,4 +7,13 @@ package plugin | |||
// | |||
// It is currently a placeholder while the implementation details are sorted out. | |||
type Context struct { | |||
SessionId string |
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.
I seem to recall @crspeller mentioning something about wanting to be able to change the wire encoding without breaking backwards compatibility. I'm not sure how that actually works, in practice, since the plugin will have compiled with this definition of Context
and would need the field to be deserialized onto it /somehow/. Does that make the accessors below redundant, given it's a public field anyway?
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.
I think the idea was to be able to make Context later on an interface without a lot of interface adaption, which won't work with fields. I am not 100% sure though, any comments @crspeller? Making the struct field private won't work because of the encoding for gob.
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.
Ya, now that I think about this, I don't think we are going to be able to accomplish that. I think we can just remove the unnecessary helpers.
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.
Helpers got removed in the latest version.
addc58c
to
7340cfd
Compare
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.
I hadn't caught up on the CSRF discussion on pre-release, so some of this might already be answered, but I threw a few questions to clarify my own understanding of the new restrictions.
Since Christopher's reviewed this code, I feel comfortable approving it, but welcome the continued discussion :)
app/plugin_requests.go
Outdated
token = cookie.Value | ||
} else { | ||
token = r.URL.Query().Get("access_token") | ||
} | ||
|
||
r.Header.Del("Mattermost-User-Id") | ||
if token != "" { | ||
if session, err := a.GetSession(token); session != nil && err == nil { | ||
session, err := a.GetSession(token) | ||
csrfCheck := true |
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.
nit: csrfCheck
read to me as check CSRF
not csrfCheckPassed
or some such.
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.
Fixed, good point.
app/plugin_requests.go
Outdated
@@ -38,22 +40,42 @@ func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) { | |||
|
|||
func (a *App) servePluginRequest(w http.ResponseWriter, r *http.Request, handler func(*plugin.Context, http.ResponseWriter, *http.Request)) { | |||
token := "" | |||
context := &plugin.Context{} | |||
|
|||
authHeader := r.Header.Get(model.HEADER_AUTH) | |||
if strings.HasPrefix(strings.ToUpper(authHeader), model.HEADER_BEARER+" ") { |
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 block of code interests me. I'm not sure why I missed it in the first round of reviews, but just some clarifying questions:
- Will I now be prohibited from making authenticated requests through a plugin's API unless I manually send the
HEADER_REQUESTED_WITH
header with the requisite value? I think the webapp does this automatically for XMLHttpRequests, but what if I'm using curl/http to hit the plugin's API, or integrating with another application that interfaces with the API? (This might be by design and/or a requirement of CSRF protection, but just want to make sure.) - What if my plugin's API endpoint isn't parsing forms, but accepts JSON? How do I
POST
a JSON payload to my plugin endpoint /and/ set thecsrf
token in the body as a form value /and/ still have an authenticated request? I guess this boils down to the previous question requiring me to set this special header? Is it a standard header, or something custom we've adopted? - It looks like we support getting an
access_token
from the query string. Ignoring any security issues with that, I guess this happens to work because we don't issue access tokens corresponding csrf tokens. Of course, someone could pass their current session's token /as/ an access token of sorts, but I suppose we don't support that.
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 you don't have the XMLHttpRequest header set you will be required to send a CSRF Token, that should not be the target when cookie authentication is used and was planned for a migration period, since that was the previous behavior. In the long-term I'd like to enforce CSRF tokens for all requests that were made with cookie auth.
- You can send a csrf token via GET parameters as well, we could add a header option optionally. The url query parameter is supported ootb by
FormValue
in Go, the header would be an additional code enhancement. - That code snippet isn't from me, but according to my understanding
access_token == session token != csrf token
- Hope that helps?
Important: I added a new check that the CSRF enforcement is only applied for cookie authentication after our chat in pre-release. FYI @crspeller
deleted test config fix test config Dont wipe the session token for plugins Simplified Tokens; Generate CSRF for other sessions Remove CSRF from Access Token; Remove Getter/Setter from Context fix removed setter remove getcsrf helper from plugin api enforce csrf only for cookie auth
7340cfd
to
697d963
Compare
I think we got now everything covered, thanks to both of you @lieut-data and @crspeller for the quick review! Looking forward to release the first alpha of Claptrap, with this PR being the last blocker for the remaining features. 🎉 |
@DSchalla Looking forward to the first release of Claptrap! |
Summary
This PR implements CSRF token which are being attached to users sessions. The CSRF tokens are rolled out and can be enforced as alternative to XHR checks in the plugin request system. The plugin API was extended to allow a simple implementation in the plugins code base.
The PR lays the foundation for the further roll out in the MM core.
Ticket Link
https://mattermost.atlassian.net/browse/MM-10346
Checklist