GioeleB00 opened a new issue, #14130:
URL: https://github.com/apache/apisix/issues/14130
### Current Behavior
### Current Behavior
The native `csrf` plugin intermittently rejects legitimate browser POSTs with
HTTP 401 and `{"error_msg":"csrf token mismatch"}` during concurrent requests
sharing the same cookie store. The client reads the current cookie
immediately
before every POST, with no cached token or `await` between the read and
`fetch`.
We reproduced this with only APISIX, a small HTTP server, and two Chromium
tabs:
no OIDC, authorization service, database, CORS, or additional proxy.
The plugin replaces the CSRF cookie on every API response, including GETs.
The observed mismatch is consistent with a concurrent response replacing the
cookie after JavaScript prepares the header but before the browser selects
the
outgoing Cookie value. We have not instrumented the browser's internal
ordering.
The impact observed is a rejected legitimate write; no CSRF bypass was
observed.
### Expected Behavior
### Expected Behavior
Provide a supported token lifecycle or client strategy that allows legitimate
concurrent writes when the client reads the cookie immediately before
`fetch`.
Invalid, expired, missing, or inconsistent tokens should still be rejected.
Is replacing the cookie on every response necessary, or could
issuance/renewal
avoid this concurrency problem?
As design context, [OWASP's HMAC CSRF token
guidance](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-hmac-csrf-tokens)
notes that per-request token updates can harm usability without adding the
assumed substantial security benefit. Could a less frequent issuance/renewal
strategy address this behavior? Initial issuance, expiry, and concurrent
renewal
would still need to be handled and tested; this is a direction for
discussion.
### Error Logs
### Error Logs
Observed HTTP response, rather than a gateway error-log excerpt:
```http
HTTP/1.1 401 Unauthorized
```
```json
{"error_msg":"csrf token mismatch"}
```
### Steps to Reproduce
### Steps to Reproduce
We prepared a standalone reproduction environment in our APISIX fork,
[GioeleB00/apisix](https://github.com/GioeleB00/apisix), on branch
`repro/csrf-concurrency`, under `repro/csrf-concurrency/`. That directory
contains
the Docker Compose stack, APISIX configuration, HTTP fixture, browser probe,
and README with setup instructions.
1. With Docker and Docker Compose available, clone the reproduction branch:
```bash
git clone --single-branch --branch repro/csrf-concurrency
https://github.com/GioeleB00/apisix.git
cd apisix/repro/csrf-concurrency
./scripts/bootstrap.sh
.venv/bin/python runner/reproduce_ui.py
```
Bootstrap prepares Python/Playwright, generates a local signing key, and
starts the two-service fixture. Port `127.0.0.1:19088` must be available.
2. The fixture serves the page and API at `http://127.0.0.1:19088`. GET
`/api/read`
obtains a native CSRF cookie; POST `/api/write` records an operation ID
only
after passing APISIX. GET `/api/stats` provides the independent write
counter.
3. The probe opens two tabs in one browser context and verifies sequential
writes.
In each concurrent trial, it launches one button click and one GET per
tab;
the second tab's group starts after a nominal delay of 10 or 0 ms. The
button
calls the original client `write(id)`, which reads the cookie and calls
native
`fetch` consecutively. Each delay has 50 trials: 200 initial POSTs in
total.
4. Inspect the sanitized report under `reports/`. In the recorded run,
**198 of 200 initial POSTs returned 201; two returned the native CSRF
mismatch
401**. Both rejected requests carried different, nonempty cookie/header
values
and had zero upstream writes. All 200 concurrent GETs returned 200.
5. The recorded evidence is included as `evidence/observed-ui-run.json`. Both
failures occurred at nominal 10 ms. The result depends on scheduling:
this is
neither a guaranteed failure rate nor a 10 ms threshold. No pause after
token
preparation, fetch interception, cookie/header override, or automatic
retry
is used.
### Environment
### Environment
- APISIX `3.18.0`, official `apache/apisix:3.18.0-ubuntu` image, unmodified
plugin.
- OpenResty `1.29.2.4`; Linux `7.0.0-38-generic`, x86_64.
- Docker Compose, two services: APISIX and Python `ThreadingHTTPServer`.
- Standalone YAML data-plane mode, one Nginx worker; no etcd or Admin API.
- Only `csrf` enabled on `/api/*`: name `apisix-csrf-token`, TTL `7200`
seconds,
dedicated environment-provided signing key. Static assets have no CSRF
plugin.
- Same-origin HTTP on loopback; browser credentials mode `same-origin`.
- Playwright `1.64.1`, headless Chromium `156.0.8078.4`, Python `3.12`
runner.
- No authentication/session plugin, Redis, `forward-auth`, or CORS plugin.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]