Reputation: 49
I'm working on a project where I've got a central API server and then multiple microservices for it including a website. The website uses OpenID to handle authentication. To allow for server-side rendering on the website yet have it remain stateless, I'm storing the access token in a cookie which is being used on the server each time the user requests a page via retrieving the access token from the cookie and appending it as an authorization header. Is there an exploits that could happen from this? As far as I'm aware I shouldn't have any problems with CSRF or any other exploit like it, however I haven't seen this way of handling authentication before.
Upvotes: 2
Views: 1561
Reputation: 3581
Short answer: Yes
Long answer
The definition of CSRF is that the authentication cookie is automatically attached when any request from anywhere to your website is made. You will always need to implement xsrf counter measures + frontend.
On each webrequest the webbrowser makes to the server, the server attaches a non-httponly cookie to the response, containing a CSRF-token which identifies the user currently signed on (NuGet).
public async Task Invoke(HttpContext httpContext)
{
httpContext.Response.OnStarting((state) =>
{
var context = (HttpContext)state;
//if (string.Equals(httpContext.Request.Path.Value, "/", StringComparison.OrdinalIgnoreCase))
//{
var tokens = antiforgery.GetAndStoreTokens(httpContext);
httpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { Path = "/", HttpOnly = false });
//}
return Task.CompletedTask;
}, httpContext);
await next(httpContext);
}
Your frontend must be configured to read this cookie (this is why it's a non-httponly cookie) and pass the csrf-token in the X-XSRF-TOKEN
header on each request:
HttpClientXsrfModule.withOptions({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN'
}),
Then you need to add and configure the Antiforgery
services to the ASP.NET Core application:
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
Now you can decorate your controller methods with the ValidateAntiforgeryAttribute.
I'm using angular, and angular does not send a X-XSRF-TOKEN
header when the URL starts with https:
. This could perhaps also be the case for React, if they provide an embedded solution.
Now if you combine this with the cookie authentication provided by ASP.NET Core Identity (SignInManager.SignInAsync
), you should be clear to go.
Note that all of the above is useless if you have an XSS vulnerability somewhere in your website. If you're not sanitizing (htmlspecialchars
) your user-input before rendering it in HTML, an attacker can manage to inject a script into your HTML:
<div class="recipe">
<div class="title">{!! Model.UnsanitizedTitleFromUser !!}</div>
<div class="instructions">{!! Model.UnsanitizedInstructionsFromUser !!}</div>
</div>
The result could possibly be the following:
<div class="recipe">
<div class="title">Pancakes</div>
<div class="instructions">
<script>
// Read the value of the specific cookie
const csrfToken = document.cookie.split(' ').map(function(item) { return item.trim(';'); }).filter(function (item) { return item.startsWith('XSRF-TOKEN'); })[0].split('=')[1];
$.delete('/posts/25', { headers: { 'X-XSRF-TOKEN': csrfToken } });
</script>
</div>
</div>
The injected script runs in the website context, so is able to access the csrf-cookie. The authentication cookie is attached to any webrequest to your website. Result: the webrequest will not be blocked.
A hacker could try and send you an email with a link to a Facebook URL. You click this link, the webbrowser opens up, the authentication cookie for facebook.com
is automatically attached. If this GET-request consequently deletes posts from your timeline, then the hacker made you do something without you realizing.
Rule of thumb: Never change state (database, login, session, ...) on a GET-request.
A second way a hacker could try and trick you is by hosting a website with the following html:
<form action="https://facebook.com/posts" method="POST">
<input type="hidden" name="title" value="This account was hacked">
<input type="hidden" name="content" value="Hi, I'm a hacker">
<input type="submit" value="Click here and earn 5000 dollars">
</form>
You only see some button on a random website with an appealing message, you decide to click it, but instead of receiving 5000 dollars, you're actually placing some posts on your facebook timeline.
As you can see, this is totally unrelated with whether you're hosting a single-page or MVC application.
In MVC websites, the usual practise is to add an input containing a CSRF token. When visiting the page, ASP.NET Core generates a CSRF token which represents your session (so if you're signed in, that's you). When submitting the form, the CSRF token in the POST body must contain the same identity as the one in the Cookie.
A hacker cannot generate this token from his website, his server since he isn't signed in with your identity.
(However, I think that a hacker would be perfectly capable of sending an AJAX GET request from his website with you visiting, then try to extract the token returned from your website and append it to the form). This could then again be prevented by excluding the GET-requests which return a form containing a CSRF-token from CORS (so basically don't have a Access-Control-Allow-Origin: *
on any url returning some CSRF-token))
This is explained on top. In each webrequest made to the server, the server attaches a non-httponly cookie to the response containing the CSRF-token for the current user session.
The SPA is configured to read this XSRF-TOKEN
cookie and send the token as X-XSRF-TOKEN
header. AFAIK, the cookie can only be read by scripts from the same website. So other websites cannot host a form
containing this token field for someone's identity.
Although the XSRF-TOKEN
cookie is also sent along to the server, the server doesn't process it. The cookie value is not being read by ASP.NET Core for anything. So when the header containing a correct token is present on the request, the backend can be sure that the webrequest was sent by your react (or in my case angular) app.
In ASP.NET Core, during a webrequest, the Identity does not change. So when you call your Login
endpoint, the middleware provided in this answer will return a csrf token for the not-signed-in user. The same counts for when you logout. This response will contain a cookie with a csrf-token as if you're still signed in. You can solve this by creating an endpoint that does absolutely nothing, and call it each time after a sign in/out is performed. Explained here
I did a little test, and this image basically summarises everything from the test:
From the image you can read the following:
So storing the token in a non-httponly cookie is only fine if the scripts you include (jquery, angularjs, reactjs, vue, knockout, youtube iframe api, ...) will not read this cookie (but they can, even when the script is included with the <script>
tag) AND you are certain that your website is fully protected against XSS. If an attacker would be somehow able to inject a script (which he hosts himself) in your website, he's able to read all non-httponly cookies of the visitors.
Upvotes: 3