asp.net-coreasp.net-web-apiantiforgerytoken

Should all the forms check the same anti-forgery token in ASP .NET Web APIs?


Here's the scenario:

The Web API uses an anti-forgery service added in Program.cs:

builder.Services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

A user wants to edit an item. So, the client requests a form from the API by calling:

[HttpGet("[action]")]
public IActionResult Edit([FromQuery] int id) 
{
    var obj = _objService.GetObjOrDefault(id);
    if (obj == null)
    {
        return NotFound();
    }

    var antiForgery = HttpContext.RequestServices.GetService<IAntiforgery>();
    if (antiForgery != null)
    {
        var tokens = antiForgery.GetAndStoreTokens(HttpContext);
        HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!, new CookieOptions { HttpOnly = false });
    }
    
    return new ObjectResult(obj);
}

The user fills the form. The client sends the updated object back to the API (with the anti-forgery token included in the header). The client calls this API method:

[HttpPut("[action]")]
[ValidateAntiForgeryToken]
public IActionResult Update([FromQuery] int id, [FromBody] SomeClass updated)
{
    _objService.UpdateObj(id, obj);
}

ASP. NET automatically checks that the anti-forgery token is valid. Awesome ... But wait a minute. The client can re-use the same anti-forgery token in all other API methods which applies the [ValidateAntiForgeryToken] attribute, seemingly with no time limit(?).

My question is, how do you ensure that the anti-forgery token was sent out for that specific form, and should you? If the same anti-forgery token can be used by the client to fill any form, why not send it once to the client when the user logs in? Do you have to clear or time out the anti-forgery token somehow?

I've basically followed this article to implement anti-forgery tokens, and it works, but I still don't feel like I understand how you should implement it.

Edit: The client may re-use the anti-forgery token across multiple controllers as long as the HTTP-context (basically the user's current URL) remains the same. You won't know from which form or which controller gave him the anti-forgery token.


Solution

  • Anti-forgery tokens are limited. They only prevent Cross-Site Request-Forgery (CSRF) attacks. By implementing anti-forgery tokens you can prove that the request was sent by a user viewing your page, and not a malicious one.

    Same-origin Policy (SOP) is a security feature implemented in web browsers. SOP ensures that a malicious site, such as www.cat.com, won't be able to read responses retrieved from your API. However, the malicious site may send requests to the API. And by default, the user's cookies, such as the session ID, are included in requests sent to the API, no matter which domain the user is currently at.

    Because of SOP, only a client using your site will be able to read the response from a GET requests. That's why we add the anti-forgery tokens when the user requests the form with e.g., GET api/something/edit. A malicious site won't be able to read the anti-forgery tokens contained in the response. So when the user later uses PUT api/something/update including the anti-forgery tokens, we know that the update request was sent from someone who was able to read the response from the GET request, and is currently browsing your site.

    As stated in the MS documentation, GET-requests should ideally have no side-effects. E.g, GET requests should not modify data, as they can be sent from malicious sites. All POST, PUT and DELETE requests should apply anti-forgery tokens, even on the sign in page.

    But, regarding my initial question: Let's say a user requests GET api/something/edit?id=1 but later sends the request PUT api/something/update?id=2. The [ValidateAntiForgeryToken] won't check that client actually updated the object he initially requested. You would need to implement some custom logic to check this, but normally you wouldn't bother to do this, since you can use Claims-based authorization and simply check if the user can update that object. With anti-forgery tokens you know that the user is performing actions while using your site and not a malicious one.

    TL:DR: To answer my initial question, "should you use the same anti-forgery token for multiple forms?". It doesn't really matter. If the user can provide the anti-forgery tokens he has proven that he is sending the request while viewing your page, and not a malicious one. The best practice is to include [ValidateAntiForgeryToken] on all POST, PUT and DELETE requests, while ensuring GET requests don't have any side effects. You still need to check that the user can update the item he's trying to modify e.g., by using Claims-based authorization.