Expired CSRF token causes JSON API mutations to fail without recovery
- Dominant language
- Python
- Stars
- 74.8k
- Forks
- 18.3k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 692
Description
### Bug description
A production incident occurred on Superset 4.0.2 when a user saved a dataset after the cached CSRF token had expired.
The JSON API mutation returned HTTP 400 with `GENERIC_BACKEND_ERROR` and Issue 1011. `SupersetClient` did not refresh the token or retry the request. The user saw a generic error and had to reload the page before saving again.
Source inspection confirms that the same behavior remains in the latest `master` branch:
1. `SupersetClient` caches and reuses the CSRF token.
2. The default token lifetime is one week.
3. Flask-WTF raises `CSRFError` after expiration.
4. JSON requests are converted into a generic HTTP 400 response.
5. `SupersetClient.request()` handles HTTP 401 but not CSRF-related HTTP 400 responses.
### Steps to reproduce
1. Configure a short CSRF lifetime:
```python
WTF_CSRF_TIME_LIMIT = 5
```
2. Restart Superset.
3. Log in and open the dataset editor.
4. Wait longer than five seconds.
5. Modify and save the dataset.
6. Inspect the browser network requests.
### Actual behavior
The save request returns HTTP 400:
```json
{
"errors": [
{
"error_type": "GENERIC_BACKEND_ERROR",
"level": "error",
"extra": {
"issue_codes": [
{
"code": 1011
}
]
}
}
]
}
```
The frontend does not request a new CSRF token or retry the save. Reloading the page obtains a new token and allows the user to save again.
### Expected behavior
Superset should recover from an expired CSRF token without requiring a page reload:
1. Return a machine-readable CSRF error type.
2. Fetch a new token from `/api/v1/security/csrf_token/`.
3. Retry the original request once.
4. Prevent refresh and retry loops.
5. Share one refresh request when several requests fail concurrently.
This does not bypass CSRF validation. The client obtains a new token for the existing authenticated session after Flask-WTF rejects the original request.
### Related work
PR #14675 added login redirection for expired CSRF tokens. The redirect applies to non-JSON requests. The JSON branch continues to return a generic HTTP 400 response.
Issue #16565 asked which status code is returned when a token expires, but it did not address frontend recovery.
### Environment
- Incident environment: Superset 4.0.2
- Current status: behavior confirmed by source inspection on `master`
- Browser: Chrome
- CSRF lifetime: default one week
- Runtime reproduction on `master`: not performed
### Additional context
Relevant source:
- Superset 4.0.2 handler:
https://github.com/apache/superset/blob/4.0.2/superset/views/base.py#L460-L490
- Superset 4.0.2 client:
https://github.com/apache/superset/blob/4.0.2/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts#L203-L215
- Current master handler:
https://github.com/apache/superset/blob/686245a905a60d8ea49b84afacfd6977a65cb5ed/superset/views/error_handling.py#L206-L240
- Current master client:
https://github.com/apache/superset/blob/686245a905a60d8ea49b84afacfd6977a65cb5ed/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts#L214-L240
Suggested implementation:
- Return a dedicated CSRF error type.
- Detect it in `SupersetClient`.
- Refresh through `reAuthenticate()`.
- Use a shared refresh promise.
- Retry the original request once.
- Preserve the existing non-JSON login redirect.
Contributor guide
Research direction
Start with superset/views/error_handling.py and superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts, comparing the current master behavior with the related login-redirection work in PR #14675. Reproduce with a five-second WTF_CSRF_TIME_LIMIT if possible, then verify that JSON mutations identify CSRF failures, refresh through the existing endpoint, retry once, share concurrent refreshes, and preserve the non-JSON redirect.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, typescript
- Domain
- api, authentication, frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100