## Ticket: CVE-2026-23958 — DataEase authentication bypass via password-derived JWT signing key

**Advisory**: OX Security (RSAC 2026 disclosure) — https://www.ox.security/blog/blog-dataease-cve-2026-23958-admin-takeover/
**CVE**: CVE-2026-23958 | **CWE-916** (Weak password hashing) / **CWE-287** (Improper authentication) / **CWE-321** (Hard-coded cryptographic key) / **CWE-798** (Use of hard-coded credentials)
**Severity**: High (NVD: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H estimate)
**Repository**: https://github.com/dataease/dataease

### Impact

DataEase (a Spring Boot data-visualization platform; the Chinese equivalent of Metabase / Superset) signs its authentication JWTs with an HMAC-SHA256 key **derived from the admin password**. In the community edition the derivation is literally `MD5(SubstituleLoginConfig.getPwd())`, and the password defaults to the documented constant `DataEase@123456` that ships with every fresh install via `dataease.default-pwd`.

An **unauthenticated, anonymous attacker** with only network access can therefore:

1. Compute `secret = MD5("DataEase@123456")`.
2. Forge a JWT with header `{alg: HS256, typ: JWT}` and payload `{uid: 1, oid: 1}`.
3. Present it as the `X-DE-TOKEN` (DataEase's auth header) and call any authenticated REST endpoint as the admin user.

This is the first link in a 4-CVE exploit chain (CVE-2026-23958 → 40899 → 40900 → 40901) that culminates in unauthenticated RCE per the OX disclosure.

### Affected / fixed versions

- **Affected**: `<= v2.10.20`
- **Fixed**: `v2.10.21` (patch commit `cac165e` — *"fix: JWT Token 漏洞"*, 2025-12-25, fit2cloud-chenyw)

Reproduce on **`v2.10.20`** and verify the fix on **`v2.10.21`**.

### Where to look

```bash
git clone https://github.com/dataease/dataease.git
cd dataease && git diff v2.10.20 v2.10.21 -- sdk/common/src/main/java/io/dataease/auth/filter/CommunityTokenFilter.java
```

The security-relevant code is one method-name change in `CommunityTokenFilter.doFilter`:

```diff
-Method pwdMethod = DeReflectUtil.findMethod(o.getClass(), "getPwd");
+Method pwdMethod = DeReflectUtil.findMethod(o.getClass(), "getSecret");
```

Plus the legacy fallback path that still hashes the substitute-login password with MD5:

```java
String pwd = SubstituleLoginConfig.getPwd();   // returns "DataEase@123456" on a default install
secret = Md5Utils.md5(pwd);
Algorithm algorithm = Algorithm.HMAC256(secret);
Verification verification = JWT.require(algorithm)
    .withClaim("uid", userId)
    .withClaim("oid", userBO.getDefaultOid());
JWTVerifier verifier = verification.build();
verifier.verify(token);
```

The default password is set in `SubstituleLoginConfig.java`:
```java
pwd = env.getProperty("dataease.default-pwd", "DataEase@123456");
```

`TokenFilter.java` (the upstream filter) extracts `uid`/`oid` claims from the JWT via `JWT.decode()` **without** verifying the signature — `CommunityTokenFilter` is the only place that actually verifies, and it verifies against the trivially-derivable secret.

### Environment

- Backend: **docker** (Spring Boot Java server + MySQL 8 + Redis required).
- The DataEase repo ships its own `docker-compose` stack under `de2-build/` / `de-base/` for each release tag. The official image registry is `registry.cn-qingdao.aliyuncs.com/dataease/dataease-ce:<tag>` (or pull from GitHub release artifacts). Either path is acceptable; the agent should pick whichever runs reliably in the sandbox.
- The default API is reachable on `http://127.0.0.1:8100` (configurable via the compose).
- Forging the JWT requires only Python stdlib (`hashlib`, `hmac`, `base64`, `json`) or any JWT library. No external network needed after the initial image pull.

### Reproduction goals — MANDATORY: live runtime exploitation

The proof MUST be the vulnerability triggering inside the live DataEase application's request-handling path, exactly as a remote attacker would reach it. Unit tests that invoke `CommunityTokenFilter` directly, reimplementations of the verification logic, mock filters, or static analysis are **NOT acceptable** and will be rejected.

1. Stand up DataEase **`v2.10.20`** via docker compose. Wait for `/de2api/user/personInfo` to respond on `127.0.0.1:8100`.
2. Anonymous baseline: `curl -i http://127.0.0.1:8100/de2api/user/personInfo` → expect **401**.
3. Forge a JWT as an **anonymous attacker** (no prior login, no credentials beyond the publicly-documented default password):
   - `secret = MD5("DataEase@123456")` (32-char lowercase hex)
   - header `{"alg":"HS256","typ":"JWT"}`, payload `{"uid":1,"oid":1}`
   - sign with HMAC-SHA256 over `base64url(header) + "." + base64url(payload)` using `secret` (as a string) as the HMAC key
4. Send the forged token to a protected API:
   ```
   curl -i -H "X-DE-TOKEN: <forged.jwt>" http://127.0.0.1:8100/de2api/user/personInfo
   ```
   Expect **200** with the admin user record in the body (admin takeover).
5. Tear down v2.10.20, bring up **`v2.10.21`** with the same compose, and replay step 4 with the *identical* forged token. Expect **401** with the `DE-GATEWAY-FLAG` header set — the patched code calls `getSecret()` instead of `getPwd()`, so the secret is no longer derivable from the default password.
6. Capture full request/response transcripts for both versions, plus the JWT and the derived MD5 secret, into the run output.

### Expected artifacts

- `repro/reproduction_steps.sh` — runnable end-to-end: brings up v2.10.20 docker compose, runs anonymous baseline + forged-token attack, captures the 200, tears down and brings up v2.10.21, replays the attack, captures the 401.
- `repro/rca_report.md` — root cause derived from the source files cited above.
- `repro/patch_analysis.md` — confirmation that `v2.10.21` commit `cac165e` neutralises the forged JWT.
- Captured HTTP transcripts and JWT details proving admin takeover on v2.10.20 and rejection on v2.10.21.
