# Variant RCA Report — CVE-2026-23958 (DataEase JWT Authentication Bypass)

## Summary

The official fix for CVE-2026-23958 (commit `cac165ee84bb296184b9be6f5fa695af0344fa05`) only patched the xpack/enterprise JWT verification path in `CommunityTokenFilter`. The community-edition fallback path, which is triggered when the proprietary `loginServer` and `apisixCacheManage` beans are absent, was left completely untouched. In this fallback path `CommunityTokenFilter` still verifies JWTs with `Md5Utils.md5(SubstituleLoginConfig.getPwd())`, and `SubstituleLoginServer` still signs them with the same secret. Because `SubstituleLoginConfig.getPwd()` defaults to the well-known string `DataEase@123456`, an unauthenticated attacker can compute the HMAC secret, forge an admin JWT, and access any authenticated REST endpoint on a pure open-source (no-xpack) build of v2.10.21. This constitutes a **bypass** of the patch.

## Fix Coverage / Assumptions

- **What the fix changes**: One line in `CommunityTokenFilter.doFilter` inside the `else` branch (`loginServer` present): `getPwd()` → `getSecret()`.
- **What the fix assumes**: All production deployments include the proprietary xpack JARs (`xpack-base.jar`, `xpack-permission.jar`, `xpack-sync.jar`), so the `loginServer` bean is always present and the `else` branch is always taken.
- **What the fix does NOT cover**:
  - The `if` branch in `CommunityTokenFilter` (`loginServer` absent).
  - `SubstituleLoginServer.localLogin`, which still signs JWTs with `Md5Utils.md5(pwd)`.
  - `SubstituleLoginConfig.getPwd()`, which still defaults to `DataEase@123456`.

## Variant / Alternate Trigger

- **Variant type**: Bypass of the official patch via an alternate deployment mode.
- **Entry point**: Any authenticated REST endpoint reachable when DataEase is deployed from the public GitHub repository (or any build where xpack JARs are stripped).
- **Trigger mechanism**:
  1. Attacker computes `secret = MD5("DataEase@123456")`.
  2. Attacker forges a JWT with claims `{uid: 1, oid: 1}` signed with `HS256(secret)`.
  3. Attacker sends the forged JWT in the `X-DE-TOKEN` header to an endpoint such as `/de2api/menu/query`.
  4. `TokenFilter` decodes the JWT (without signature verification) and extracts `uid`/`oid`.
  5. `CommunityTokenFilter.doFilter` reaches the `if` branch because `CommonBeanFactory.getBean("loginServer")` is empty.
  6. The filter derives `secret = Md5Utils.md5(SubstituleLoginConfig.getPwd())`, which equals the attacker's secret, so the JWT passes verification.
  7. The request proceeds as the admin user.

## Impact

- **Package/Component**: `io.dataease.auth.filter.CommunityTokenFilter` (sdk/common) and `io.dataease.substitute.permissions.login.SubstituleLoginServer` (core/core-backend)
- **Affected versions**: v2.10.21 when built/deployed without proprietary xpack JARs (pure open-source/community edition).
- **Risk level**: High — identical to the original CVE: unauthenticated remote attacker can impersonate the admin user.
- **Consequences**: Full admin takeover, enabling exploitation of downstream authenticated endpoints.

## Root Cause

The fix changed only the xpack path:

```java
// CommunityTokenFilter.java (v2.10.21)
if (ObjectUtils.isEmpty(CommonBeanFactory.getBean("loginServer"))) {
    // UNCHANGED — still uses default password hash as secret
    String pwd = SubstituleLoginConfig.getPwd();
    secret = Md5Utils.md5(pwd);
} else {
    // FIXED — now uses getSecret() instead of getPwd()
    Object apisixCacheManage = CommonBeanFactory.getBean("apisixCacheManage");
    ...
    Method pwdMethod = DeReflectUtil.findMethod(o.getClass(), "getSecret");
    ...
}
```

And the community login controller remains unchanged:

```java
// SubstituleLoginServer.java (v2.10.21)
String md5Pwd = Md5Utils.md5(pwd);
return generate(tokenUserBO, md5Pwd);
```

When xpack is absent, both signing and verification use the trivially derivable `MD5(default_password)` secret.

## Reproduction Steps

1. Run `vuln_variant/reproduction_steps.sh`.
2. The script builds a `dataease:v2.10.21-no-xpack` image by removing the proprietary xpack JARs from the official v2.10.21 Docker image.
3. It starts a MySQL container and the no-xpack DataEase container.
4. It sends a forged JWT (signed with `MD5("DataEase@123456")`) to `/de2api/menu/query`.
5. **Expected result**: HTTP 200 with actual menu data (bypass confirmed).
6. It then starts the regular v2.10.21 DataEase (with xpack JARs) and repeats the request.
7. **Expected result**: HTTP 401 with `DE-GATEWAY-FLAG: The Token's Signature resulted invalid...` (fix works).

## Evidence

- `logs/variant_test.log` — Full execution transcript showing both test runs.
- `logs/no_xpack_bypass_headers.txt` — HTTP 200 response headers from no-xpack v2.10.21.
- `logs/no_xpack_bypass_body.txt` — JSON menu data returned (proof of authenticated access).
- `logs/no_xpack_baseline_headers.txt` — HTTP 401 baseline without token.
- `logs/full_bypass_headers.txt` — HTTP 401 response from regular v2.10.21 with same forged JWT.
- `logs/docker_build_no_xpack.log` — Docker build log for the no-xpack image.

Key excerpts:

```
No-xpack v2.10.21 forged-JWT response status: 200
No-xpack v2.10.21 baseline response status: 401
Regular v2.10.21 forged-JWT response status: 401
```

## Recommendations / Next Steps

1. **Extend the fix to the community fallback path**: In `CommunityTokenFilter.doFilter`, replace `Md5Utils.md5(SubstituleLoginConfig.getPwd())` with a per-instance random secret (e.g., generated on first startup and persisted in `substitule.json`).
2. **Synchronize `SubstituleLoginServer` signing secret**: Ensure `SubstituleLoginServer.generate()` uses the same per-instance random secret, not `Md5Utils.md5(pwd)`.
3. **Deprecate password-derived JWT secrets entirely**: Whether in xpack or community mode, the JWT signing key must never be derivable from any user-facing value, including the admin password.
4. **Add a startup secret generation step**: If `substitule.json` does not exist, generate a cryptographically random secret instead of defaulting to a hardcoded password.

## Additional Notes

- **Idempotency**: The reproduction script was executed twice (run1 and run2) and produced identical results (200 bypass on no-xpack, 401 rejection on regular) in both runs.
- **Trust boundary**: The attacker only needs network access to the DataEase server; no local file access or prior authentication is required.
- **Limitation**: The bypass only manifests when the proprietary xpack JARs are absent from the classpath. However, this is the default state for anyone building from the public GitHub repository, making it a significant exposure.
