Page MenuHomePhabricator

System Administrator avoids CSRF attacks on MediaWiki REST API
Closed, ResolvedPublic

Description

User story

"As an System Administrator, I want the MediaWiki REST API to avoid cross-site request forgery attacks, to preserve the privacy of our users and integrity of our data."

Problem

In building the new MediaWiki REST API, we've run into a design problem.

The Action API uses CSRF tokens for many write functions, like creating or updating a page. So far, we can't find another public API that uses CSRF tokens. We don't want to add a requirement for developers to provide a CSRF token if they’re not needed.

It looks like the Action API requires CSRF tokens because it supports session cookie authorization.

For the MediaWiki REST API, we will typically support OAuth 1.0 or OAuth 2.0 for authorization.

However, we may need to support session cookie authorization for the official web clients.

Proposed solution

For the MediaWiki REST API, we’ll add an optional CSRF token parameter for any endpoint that modifies state (typically POST, PUT, or DELETE endpoints). If it is called with session cookie authorization, we require the CSRF token to be provided additionally.

Another solution is to disallow authorization to the MW REST API with session cookies. This would be awesome, but there's some opposition to doing it in our group.

We'll need to work this out in order to resolve T232176, also. Making it easy for other sites to call our public REST API is much, much more important for the API than supporting session cookie authorization.

Event Timeline

I added this ticket so we have one place to hash out issues on CSRF tokens. BY FAR, my preference is to support only OAuth authorization and enable CORS.

I added this ticket so we have one place to hash out issues on CSRF tokens. BY FAR, my preference is to support only OAuth authorization and enable CORS.

So to clarify, one of the expected use cases is that third party sites will make (cross origin) authenticated write requests to the api, entirely on the client side using ajax with CORS, well being authenticated as one of our users?

If so, i think we should think about this authorization workflow conceptually. How does a user tell the third party site that they want it to make api requests on its behalf? Is there a special login flow? OAuth prompt flow? Are we supporting both our current oAuth & oAuth2? I think there is going to be an extreme amount of devil in the details here depending on what we want to support

I added this ticket so we have one place to hash out issues on CSRF tokens. BY FAR, my preference is to support only OAuth authorization and enable CORS.

So to clarify, one of the expected use cases is that third party sites will make (cross origin) authenticated write requests to the api, entirely on the client side using ajax with CORS, well being authenticated as one of our users?

Yes. Authenticated read requests are also an issue, especially for private wikis or for high-level users and deleted content on public wikis.

Is there a special login flow? OAuth prompt flow?

OAuth.

Are we supporting both our current oAuth & oAuth2?

OAuth 1.0 for some period of time, moving exclusively to OAuth 2.0 at some point.

I think there is going to be an extreme amount of devil in the details here depending on what we want to support

I agree, but that's Web security, right? I think if the MW REST API ignores session cookies entirely, we simplify a lot of this.

So the current API mostly works under the assumption that authenticated CORS requests come from trusted (i.e. WMF operated) websites only. To use cookie authentication w/CSRF tokens for other websites is probably possible in theory, but would involve a much more complicated flow. I don't really think we should do that unless we have to.

For OAuth (1.0), the current situation is that it is assumed that oAuth authenticated requests come from the server side (of the third party api), and not the clients (In particular, the authorization header is not currently in the allowed header list). For oAuth to be secure, the application secret should not be exposed to the client. The calculation of signature/hmac can in principle be done on the server side, and then transmitting it to the client for transmitting over ajax, and I think (Just looking at a glance) that that would be secure. However it would also be a rather convoluted, and I imagine that people would try and cut-corners and do the calculation on the client side if they're going to make requests via ajax anyways, which is insecure.

Here are some thoughts I have...

  1. It doesn't really matter what the private API does. I suppose this is a question if we can make such a distinction or not. Are all our APIs "public" or are some of them "private" (private as in, limited to the current origin)?
  2. An option could be.. if a CSRF token is provided, then load the session from the cookie. In this way it's 100% required, but if it's missing (which would be the case for public requests... then that is fine, the cookie is ignored).
  3. Instead of making an endpoint to get the token, perhaps it would be better to embed the token on the HTML page itself? If the only use case to do this is JavaScript on the same origin, then I don't see why you would need to make another request just to get a token. Unless you can have a session, but get a cached page in MediaWiki? I don't think that is possible... but I might be missing something (I realized this doesn't matter, you only have to hit an uncached page a single time for the token to be loaded into LocalStorage to be used at any point in the future)
  4. If you have to pass a token for each request.... why look at the Cookies/Session at all? Why not just have a "token" (a JWT or whatever) that contains everything needed to authorize the user?
  5. Taking this yet another step further... why not embed an OAuth 2 refresh/access token on the page... and authorize with OAuth 2 like everyone else?
  6. @Bawolff see https://www.oauth.com/oauth2-servers/single-page-apps/ for how OAuth 2 can be accomplished without a server or an app secret. Effectively it relies on HTTPS (and the server) giving the client id and secret to only the registered app domain (via a redirect). In this way, unless the app gives away the client secret (or the client does) requests from that client id and secret are known to come only from that application. I'm not sure if this can be accomplished with OAuth 1.0 as it does not require HTTPS (afaik).

@Bawolff see https://www.oauth.com/oauth2-servers/single-page-apps/ for how OAuth 2 can be accomplished without a server or an app secret. Effectively it relies on HTTPS (and the server) giving the client id and secret to only the registered app domain (via a redirect). In this way, unless the app gives away the client secret (or the client does) requests from that client id and secret are known to come only from that application. I'm not sure if this can be accomplished with OAuth 1.0 as it does not require HTTPS (afaik).

To be clear, my previous comment applied only to oAuth 1, oAuth2 is a really different protocol (its not really the next version so much as two totally different protocols). It sounds likely that oAuth2 would be more applicable, but im not as familar with it as oauth1, so i wouldnt want to comment without reading through the spec to refresh my memory

To be clear, my previous comment applied only to oAuth 1, oAuth2 is a really different protocol (its not really the next version so much as two totally different protocols). It sounds likely that oAuth2 would be more applicable, but im not as familar with it as oauth1, so i wouldnt want to comment without reading through the spec to refresh my memory

Ah. no problem. I'm somewhat certain that you can't make an OAuth 1.0 request without a server, in which case, it would be somewhat pointless requiring OAuth authorization from the same-origin. I guess you would have to have some other proxy that could only be used on that origin or something.... If the choices, on the same origin, are between OAuth 1.0 and something else, I would choose something else. :) I think OAuth 2 should satisfy everyone's requirements though. Regardless a token that contains the authorization (like a JWT) could be used on the same-origin (since you have to have a CSRF token anyways, not sure sure why you wouldn't embed the entire authorization information and ignore the cookie ok, I guess you would only need the CSRF for write operations where the cookies can be used blindly for read).

technically you don't need CSRF tokens with a REST API (assuming you are using JSON or XML) because the write request is no longer a simple request and therefore will trigger a preflight request. Since that preflight request would fail on a cross-origin request (without the OAuth 2 token), then there isn't a way to send the data.

I would have the server validate that the Content-Type header (and respond with a 415 if it's not?) is one of the allowed types and does not blindly accept a content type that isn't allowed. Doing so will prevent the request from being marked as "simple."

In summary, you can use the sessions without a problem, and without a need for a CSRF token. ;)

I hope this solves your problem. :)

@eprodromou welp! that explains why none of the REST APIs you looked at use CSRF tokens. 😀

Are we supporting both our current oAuth & oAuth2?

OAuth 1.0 for some period of time, moving exclusively to OAuth 2.0 at some point.

For the REST API, you may as well require 2.0 from the start, if you're wanting it to not be usable in the ways our existing clients would expect based on all our existing APIs. Why bother having clients implement with OAuth 1.0a only to make them reimplement with 2.0 soon after?

For the Action API, we already have clients using 1.0a. The process of deprecating and removing OAuth 1.0a support should be measured in years, if we bother to do it at all.

  1. @Bawolff see https://www.oauth.com/oauth2-servers/single-page-apps/ for how OAuth 2 can be accomplished without a server or an app secret. Effectively it relies on HTTPS (and the server) giving the client id and secret to only the registered app domain (via a redirect). In this way, unless the app gives away the client secret (or the client does) requests from that client id and secret are known to come only from that application. I'm not sure if this can be accomplished with OAuth 1.0 as it does not require HTTPS (afaik).

The method described at that link protects the end user from a malicious third party (in most cases), but how can MediaWiki actually know that it really came from the registered app? What if an attacker does something like this?

  1. A malicious app exists that uses a client ID extracted from a legitimate app that uses the linked method.
  2. Complicit end user uses the malicious app to initiate the OAuth 2 authorization flow.
  3. At the end of the authorization flow, MediaWiki redirects to the legitimate app's redirect URI.
  4. The complicit end user intercepts the redirect, passing the data to the malicious app instead so it can complete the authorization process.

Everything I've been able to find so far focuses on protecting non-complicit end users from a malicious app, mostly by asserting that "the environment" must prevent a malicious app from doing #4 without the user's knowledge.

Our implementation of 1.0a includes some of the things 2.0 added to try to make this method less insecure, including restriction to preregistered redirect URIs.

  1. The complicit end user intercepts the redirect, passing the data to the malicious app instead so it can complete the authorization process.

Do you mean someone would manually intercept the redirect and pass it to another application? I think this would assume that the legitimate app doesn't again redirect the user as a safety measure, removing the credentials from the URL. If the legitimate app does this, then it's much more difficult for a complicit user to take the redirect URL and re-use it (though, we can't enforce this best practice).

Regardless, there isn't much that can protect complicit users from any authentication mechanism. What if a malicious app asks for my Wikimedia username and password directly? How do we protect against that now?

Our implementation of 1.0a includes some of the things 2.0 added to try to make this method less insecure, including restriction to preregistered redirect URIs.

Right, we basically do this already, except for enforcing HTTPS (?) which would prevent the potential man in the middle and requiring the client (app) secret (which we would have to stop doing to allow client applications to authorize directly). At that point, right, why not just use OAuth 2 for the REST API. :)

Do you mean someone would manually intercept the redirect and pass it to another application? I think this would assume that the legitimate app doesn't again redirect the user as a safety measure, removing the credentials from the URL. If the legitimate app does this, then it's much more difficult for a complicit user to take the redirect URL and re-use it (though, we can't enforce this best practice).

Even if the legitimate app would redirect, the complicit user doesn't need to allow their user agent to make the request in the first place.

Regardless, there isn't much that can protect complicit users from any authentication mechanism. What if a malicious app asks for my Wikimedia username and password directly? How do we protect against that now?

My point was that the statement "requests from that client id and secret are known to come only from that application" is not accurate.

Right, we basically do this already, except for enforcing HTTPS (?)

For Wikimedia wikis we force HTTPS for everything.

At that point, right, why not just use OAuth 2 for the REST API. :)

The main reason is that existing clients already have implementations for the existing authentication mechanisms, and often enough the volunteer maintainers don't have the time to make significant updates like rewriting their authentication code to use OAuth 2.

There aren't existing clients of the REST API, true, but it would still be a hoop for maintainers of existing Action API clients to jump through if they want to start using a REST endpoint too.

My point was that the statement "requests from that client id and secret are known to come only from that application" is not accurate.

I did add the qualifier: "unless the app gives away the client secret redirect code (or the client user does)" There isn't anything (that I know of) that prevents a user from giving away their own credentials (even if that means giving away a username and password). The only way to prevent it, is to not have credentials at all. :)

For Wikimedia wikis we force HTTPS for everything.

I know, but do we force HTTPS on the redirect? Or am I allowed to redirect to a non-HTTPS site?

The main reason is that existing clients already have implementations for the existing authentication mechanisms, and often enough the volunteer maintainers don't have the time to make significant updates like rewriting their authentication code to use OAuth 2.

There aren't existing clients of the REST API, true, but it would still be a hoop for maintainers of existing Action API clients to jump through if they want to start using a REST endpoint too.

I would argue that it's easier to implement OAuth 2, but yes, this is true, it is a departure from what they are currently doing. :)

I wrote about this topic at T234665#5655122. The summary is that I think OAuth 2.0 grant_type=client_credentials can be authenticated using session cookies. From the client's point of view, fetching an access token using client_credentials and submitting it back in order to authenticate a write request is basically the same as using a CSRF token. In OAuth 2.0, there's no MAC calculation, tokens are just "bearer" tokens which are replayed back to the server without modification, so clients are much simpler to implement.

My proposal is to add support for client_credentials to MW core, along with a framework for basic OAuth 2.0 concepts, like allowing core to determine whether or not a client is authenticated via an OAuth access token. Client registration and the implementation of authorization code grants would be added to the OAuth extension, allowing OAuth 2.0 clients to be listed in the UI alongside OAuth 1.0 clients.

I wrote about this topic at T234665#5655122. The summary is that I think OAuth 2.0 grant_type=client_credentials can be authenticated using session cookies.

Absolutely. As far as I know, OAuth does not cover the authentication mechanism, only authorization. The server can use whatever authentication it wishes. :)

From the client's point of view, fetching an access token using client_credentials and submitting it back in order to authenticate a write request is basically the same as using a CSRF token.

Not at all, A CSRF token is to protect against cross-site request forgery, it does not contain the data necessary to authorize the request. In other words, you need the CSRF token and the Cookie header. Whereas providing an access_token is all you need with OAuth.

I suggested in T237852#5653833 that a token could be used that does contain this data (and does not need to be an OAuth token). But, to be fair, this data is already in the Cookie header. The only reason the CSRF token is necessary is because the request is a "Simple" request (as I explained in T237852#5654079).

Ensuring the requests are non-simple, removes the need for CSRF tokens. I think that's the way forward with this task and avoids the OAuth requirement.

Not at all, A CSRF token is to protect against cross-site request forgery, it does not contain the data necessary to authorize the request. In other words, you need the CSRF token and the Cookie header. Whereas providing an access_token is all you need with OAuth.

That's why I said "basically". The fact that access tokens are sufficient for authorization was not relevant to my point, which was that OAuth 2.0 is not significantly more difficult for clients to implement than traditional CSRF tokens.

That's why I said "basically". The fact that access tokens are sufficient for authorization was not relevant to my point, which was that OAuth 2.0 is not significantly more difficult for clients to implement than traditional CSRF tokens.

Ah! I understand now. Yes, I quite agree. Especially if we embed the refresh/access token in the HTML (on non-cached requests). :)

Aklapper renamed this task from System Adminstrator avoids CSRF attacks on MediaWiki REST API to System Administrator avoids CSRF attacks on MediaWiki REST API.Nov 14 2019, 11:13 PM

Change 578666 had a related patch set uploaded (by BPirkle; owner: BPirkle):
[mediawiki/core@master] Allow SessionProviderInterface to say if it needs CSRF protection

https://gerrit.wikimedia.org/r/578666

daniel set Final Story Points to 1.
daniel removed Final Story Points.

Change 578683 had a related patch set uploaded (by BPirkle; owner: BPirkle):
[mediawiki/extensions/OAuth@master] Added new function safeAgainstCsrf to MWOAuthSessionProvider

https://gerrit.wikimedia.org/r/578683

Change 578666 merged by jenkins-bot:
[mediawiki/core@master] Allow SessionProviderInterface to say if it is safe against CSRF

https://gerrit.wikimedia.org/r/578666

Change 578683 merged by jenkins-bot:
[mediawiki/extensions/OAuth@master] Added new function safeAgainstCsrf to MWOAuthSessionProvider

https://gerrit.wikimedia.org/r/578683

Change 579260 had a related patch set uploaded (by Daniel Kinzler; owner: Daniel Kinzler):
[mediawiki/core@master] REST: page/ endpoints: don't use tokens with OAuth

https://gerrit.wikimedia.org/r/579260

Change 579260 merged by jenkins-bot:
[mediawiki/core@master] REST: page/ endpoints: don't use tokens with OAuth

https://gerrit.wikimedia.org/r/579260