apache / apache/superset

Expired CSRF token causes JSON API mutations to fail without recovery

Open
#43,550 2 comments 0 reactions 0 assignees View on GitHub
api authentication change:backend change:frontend
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.