Recently I am migrate my previous website for “Lieben in Deutschland” test frontend to another cloud. And build for my new backend demos website. But during these, I met with some issue with CSRF, and then I found this interesting topic of “third-party cookie blocking”. By this blog I want to record some steps of solving these issue.
1. CSRF
Let’s first have a quick review of CSRF attack and its solution.
1.1 CSRF explaination
We use CSRF as the case, which is the “Cross Site Request Forgery”, where the attacker can utilize the login user’s authenticated browser stored cookies to forge/trick a request and send to the server.
1.2.1 X-XSRF-Token
As the cookie can be utilized to authenticate the malicious form submission, so we can add a token in both the cookie and the Headers.
Even the cookies token stored cannot be check by the attacker, but it can still be used to send to the server, so what we actually need, is checking the token inside the Header with the session token on the server.
The Session Link Always Works: The Session Cookie sent by the browser always matches the server's session store (assuming the user is logged in). This cookie is what links the incoming request to the victim's authenticated session on the server.
The Token Mismatch is the Defense: The server checks if the token inside the request body/header matches the token stored within the server-side session data (which the Session Cookie points to).
1.2.2 .Net solution
in .Net we use Antiforgery
, where we can add the Header name as:
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
});
and for MVC we can directly use the middleware ValidateAntiForgeryToken
, and for web api, we can use the IAntiforgery
for the validation.
[ApiController]
[Route("api/[controller]")]
public class CsrfDemoController : ControllerBase
{
private readonly IAntiforgery _antiforgery;
public CsrfDemoController(IAntiforgery antiforgery)
{
_antiforgery = antiforgery;
}
[HttpGet("token")]
public IActionResult GetCsrfToken()
{
var tokens = _antiforgery.GetAndStoreTokens(HttpContext);
return Ok(new { csrfToken = tokens.RequestToken });
}
[HttpPost("secure-action")]
public async Task<IActionResult> SecureAction([FromBody] dynamic data)
{
// diff from MVC: Manual validation here, not using
// [ValidateAntiForgeryToken]
await _antiforgery.ValidateRequestAsync(HttpContext);
return Ok(new { message = "CSRF token validated!", input = data });
}
}
ASP.NET Core's AddAntiforgery
validation requires BOTH:
A cookie with a value
A header/form field with a matching value
It validates that they match.
1.2.3 JWT, etc
Here we just one line notice that actually if we use JWT or other more secure way for login authentication, the CSRF will be avoid. This is not our topic here.
2. Occurence
How this related to the third party cookie blocking issue of browsers, for this I need to move to my recently re-deployment. In order to give a clear workflow, let’s focus on the main services, (So here we don’t mention API Gateway or other secure settings, also let DB alone). Use a simple and clean structure.
2.1 Basic Architecture
My new deployment is:
Backend: AWS Lambda (.Net 8)
Frontend: Netlify (React)
the backend code basically is like above .Net code, the frontend just some fetch
calls just like:
// GET
await fetch(`${apiUrl}/api/CsrfDemo/token`, {
method: 'GET',
credentials: 'include', // very important for cookie
});
// POST
await fetch(`${apiUrl}/api/CsrfDemo/secure-action`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken, // antiforgery header, set in .net code
},
credentials: 'include', // sends the cookie
body: JSON.stringify({ message: 'CSRF test from React' }),
});
2.2 What happening?
Before the deployment, I tested all the APIs locally, all APIs work well, CSRF API either.
But after the deployment, I found CSRF API response in the browser will show me server error like:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
"title": "An unexpected error occurred",
"status": 500,
"detail": "The required antiforgery cookie \".AspNetCore.Antiforgery\" is not present.",
"traceId": "00-53a3ef57a071175d1c7dd256494f9d2c-1af0e8de357b09p7-00"
}
it told me .AspNetCore.Antiforgery
is not shown in my cookie.
That’s wired, because locally it test well, and even after deployment, when I use curl
to call CSRF API, it also works well and show .AspNetCore.Antiforgery
in cookies.
# 1. Get token and save cookies
curl -v -c cookies.txt \
-H "Origin: https://myfrontend-addr" \
https://my-lambda-url/api/CsrfDemo/token
# Check if cookies.txt has the antiforgery cookie
cat cookies.txt
# 2. Use the saved cookies
curl -v -b cookies.txt \
-X POST \
-H "Content-Type: application/json" \
-H "X-XSRF-TOKEN: <token-from-above>" \
-H "Origin: https://myfrontend-addr" \
-d '{"data": "test"}' \
https://my-lambda-url/api/CsrfDemo/secure-action
and the cookies.txt
file show me:
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_mylambda.lambda-url.on.aws FALSE / TRUE 0 .AspNetCore.Antiforgery abcdefg1234567890
it shows clearly the .AspNetCore.Antiforgery
, which include the CSRF token eg. “abcdefg1234567890”.
So for now, as locally test successfully, my backend code logic is fine, and since curl works, this confirms my Lambda backend is configured correctly. The issue maybe browser-specific, browsers have stricter cookie policies than curl, especially for cross-origin requests.
2.3 Third Party cookies blocking (not store)
Let’s see the browser request and response.
By checking the Network, we found the for the GET token API response shows:
HTTP/1.1 200 OK
Date: Wed, 15 Oct 2025 16:42:20 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 171
Connection: keep-alive
x-amzn-RequestId: 1234567890
access-control-allow-origin: https://my-frontend-url
x-frame-options: SAMEORIGIN
Set-Cookie: .AspNetCore.Antiforgery=abcdefg1234567890; path=/; secure; samesite=none; httponly
cache-control: no-cache, no-store
X-Amzn-Trace-Id: Root=abcd;Sampled=0;Lineage=1:80aab7f3:0
pragma: no-cache
access-control-allow-credentials: true
where we see Set-Cookie
Header and the .AspNetCore.Antiforgery
with CSRF token, eg. “abcdefg1234567890”.
while as previous React
code we also see we already used:
credentials: 'include',
which should include the cookie, and browser then stored the cookie.
But when we check the cookies in the browser, we found empty, which means the browser hasn’t store the cookie.
This is different from when we check the local deployment case, in local testing, after the get token, we will found the cookies are stored with the name .AspNetCore.Antiforgery
, which is same as what we see in above curl
cmd.
So here comes the third Party cookies blocking.
Actually the name third party cookies blocking actually is the browser will not store the cookies from third party.
3. A simple Solution
Now our situation is:
GET request (works):
Browser sends GET to Lambda
Lambda responds with
Set-Cookie: .AspNetCore.Antiforgery=...
Browser receives it BUT doesn't store it (third-party cookie blocking)
However, the response body still contains the token as JSON
POST request (fails):
Browser has no cookie stored (because it was blocked)
Browser sends POST with
X-XSRF-TOKEN
headerLambda checks: "Where's the cookie?" → Not found → Error
The Set-Cookie
appears in response headers, but the browser silently refuses to store it.
The reason caused this is about our .Net code, as we point out previous that ASP.NET Core's AddAntiforgery
validation requires BOTH:
A cookie with a value
A header/form field with a matching value
It validates that they match.
But since the cookie isn't stored, validation will fail.
3.1 Token comparison only from Headers
As the AddAntiforgery
validate fails as the cookie are not stored, but we still want to validate the token, why not change the logic to only check the tokens from header manually, as the token in cookies will always same.
So we extract the token from Header X-XSRF-TOKEN
and compare with the backend stored token.
Therefore we have the method re-write:
using Microsoft.AspNetCore.Mvc;
namespace backend.CSRF.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class CsrfDemoController : ControllerBase
{
// In-memory token storage, if focus on concurrency can use Redis to store
private static readonly Dictionary<string, DateTime> _tokens = new();
private static readonly object _lock = new();
[HttpGet("token")]
public IActionResult GetCsrfToken()
{
// Generate random token by ourselves manully
var token = Guid.NewGuid().ToString() + Guid.NewGuid().ToString();
lock (_lock)
{
_tokens[token] = DateTime.UtcNow.AddMinutes(10);
}
return Ok(new { csrfToken = token });
}
[HttpPost("secure-action")]
public IActionResult SecureAction([FromBody] dynamic data)
{
// Get token from header manully
var token = Request.Headers["X-XSRF-TOKEN"].FirstOrDefault();
if (string.IsNullOrEmpty(token))
return BadRequest(new { error = "Missing CSRF token" });
lock (_lock)
{
// Check if token exists and is not expired
if (!_tokens.TryGetValue(token, out var expiresAt))
return BadRequest(new { error = "Invalid or already-used CSRF token" });
if (DateTime.UtcNow > expiresAt)
{
_tokens.Remove(token);
return BadRequest(new { error = "CSRF token expired" });
}
// Remove token (single-use)
_tokens.Remove(token);
}
return Ok(new { message = "CSRF token validated!", input = data });
}
}
}
now the token from GET method will be checked on the Network, Preview Tab, and in the POST method Request Headers, we can also find the X-XSRF-TOKEN
header.
Summary
In this article we use a simplified CSRF case to show this third party cookie blocking in Browser issue, and how to solve this problem via a simplified way. Actually, there are many more secure and better way to solve this issue, there are many article talk about it, eg. Microsoft Learn (How to handle third-party cookie blocking in browsers). For this article just a record of what issue I have met with during the re-deployment.
Top comments (0)