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]

Reply via email to