CVE-2026-23958: Verified Repro With Script Download
CVE-2026-23958: DataEase: authentication bypass via password-derived HMAC JWT signing key
CVE-2026-23958 is verified against dataease · github. Affected versions: <= v2.10.20. Fixed in v2.10.21. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00168.
What Is CVE-2026-23958?
CVE-2026-23958 is a high-severity authentication-bypass vulnerability in DataEase, a Spring Boot data-visualization platform, caused by signing authentication JWTs with a key derived from the admin password. Pruva reproduced it (reproduction REPRO-2026-00168).
CVE-2026-23958 Severity & CVSS Score
CVE-2026-23958 is rated critical severity, with a CVSS base score of 9.8 out of 10.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
Affected dataease Versions
dataease · github versions <= v2.10.20 are affected.
How to Reproduce CVE-2026-23958
pruva-verify REPRO-2026-00168 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00168/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-23958
Reproduced by Pruva's autonomous agents — 630 tool calls over 1h 40m. Full root-cause analysis and the complete transcript are below.
The official v2.10.21 fix for CVE-2026-23958 only patched the xpack/enterprise JWT verification path (getPwd -> getSecret). The community-edition fallback path in CommunityTokenFilter and SubstituleLoginServer was left untouched. When DataEase is deployed without proprietary xpack JARs (pure open-source build), the JW…
How the agent worked
Root Cause and Exploit Chain for CVE-2026-23958
DataEase signs its authentication JWTs with an HMAC-SHA256 key derived from the admin password. In vulnerable versions, the CommunityTokenFilter uses getPwd() (which returns the raw password hash) as the JWT verification secret. Because the default admin password is the well-known constant DataEase@123456, an unauthenticated attacker can compute MD5("DataEase@123456"), forge a JWT with claims {uid:1, oid:1}, and present it in the X-DE-TOKEN header to access any protected REST endpoint as the admin user.
- Package/Component:
io.dataease.auth.filter.CommunityTokenFilter(sdk/common) andio.dataease.xpack.permissions.login.bo.LoginUserCacheBO(xpack-permission) - Affected versions: Docker images up to and including v2.10.10 are demonstrably vulnerable. Git commit analysis shows the fix (
getPwd→getSecret) was already merged by v2.10.20, but the ticket incorrectly labels v2.10.20 as vulnerable. - Risk level: High — unauthenticated remote attacker can impersonate the admin user.
- Consequences: Full admin takeover via forged JWT, enabling subsequent exploitation of authenticated endpoints.
Root Cause
In the CommunityTokenFilter.doFilter method, when the application is running with an active loginServer bean (the standard Docker image configuration), the filter reaches the else branch and derives the JWT secret from the user cache object:
Object apisixCacheManage = CommonBeanFactory.getBean("apisixCacheManage");
Method method = DeReflectUtil.findMethod(apisixCacheManage.getClass(), "userCacheBO");
Object o = ReflectionUtils.invokeMethod(method, apisixCacheManage, userId);
Method pwdMethod = DeReflectUtil.findMethod(o.getClass(), "getPwd"); // vulnerable
Object pwdObj = ReflectionUtils.invokeMethod(pwdMethod, o);
secret = pwdObj.toString();
In the vulnerable code (getPwd), secret is simply the user's password hash (504c8c8dfcbbe5b50d676ad65ef43909 for the default admin). This is trivially derivable by anyone who knows the default password. The fix changes getPwd to getSecret, which concatenates the password hash with the per-installation RSA public key, making the secret unpredictable and no longer derivable from public information alone.
Fix commit: cac165ee84bb296184b9be6f5fa695af0344fa05 ("fix: JWT Token 漏洞", 2025-12-25). This commit is already present in git tag v2.10.20 and in Docker images v2.10.20+.
Reproduction Steps
- Run
repro/reproduction_steps.sh - The script:
- Starts a MySQL 8 container and a DataEase v2.10.10 container (vulnerable)
- Waits for the API to respond on
http://127.0.0.1:8100 - Baselines an anonymous request to
/de2api/user/personInfo→ expects 401 - Forges a JWT with
secret = MD5("DataEase@123456")and sends it asX-DE-TOKEN→ expects 200 - Stops the vulnerable app but preserves the MySQL data
- Starts a DataEase v2.10.21 container against the same MySQL data
- Replays the identical forged JWT → expects 401 with
DE-GATEWAY-FLAGheader
- Expected evidence:
logs/vulnerable_attack_response.txtshows HTTP 200logs/fixed_attack_response.txtshows HTTP 401 andDE-GATEWAY-FLAG: The Token's Signature resulted invalid...
Evidence
logs/repro_run1.log— first successful execution ofreproduction_steps.shlogs/repro_run2.log— second successful execution (idempotency confirmed)logs/vulnerable_transcript.txt— summary of v2.10.10 test resultslogs/fixed_transcript.txt— summary of v2.10.21 test resultslogs/vulnerable_attack_response.txt— raw HTTP response showing 200 on forged JWTlogs/fixed_attack_response.txt— raw HTTP response showing 401 +DE-GATEWAY-FLAG
Key excerpts from v2.10.10 (vulnerable):
HTTP/1.1 200
X-DE-EXECUTE-VERSION: 2.10.10
...
{"code":60003,"msg":"缺少许可证","data":null}
(The 200 status proves the JWT signature was accepted; the downstream "missing license" error is irrelevant to the auth bypass.)
Key excerpts from v2.10.21 (fixed):
HTTP/1.1 401
X-DE-EXECUTE-VERSION: 2.10.21
DE-GATEWAY-FLAG: The%20Token%27s%20Signature%20resulted%20invalid%20when%20verified%20using%20the%20Algorithm%3A%20HmacSHA256
Recommendations / Next Steps
- Upgrade to DataEase v2.10.20 or later. The fix commit is already present in those builds.
- Rotate secrets: If running an older vulnerable build, change the admin password and restart the application so that any cached JWT secret is regenerated.
- Additional hardening: Remove the fallback MD5-based secret derivation in
SubstituleLoginConfigentirely, or enforce a randomly generated community-edition signing key at first boot. - Regression testing: Add an integration test that attempts to authenticate with a JWT signed using only
MD5(default_password)and asserts 401.
Additional Notes
- Idempotency:
reproduction_steps.shwas executed twice consecutively with identical results (HTTP 200 on v2.10.10, HTTP 401 on v2.10.21). - Version discrepancy: The ticket specifies v2.10.20 as vulnerable and v2.10.21 as fixed. However, binary analysis of the official Docker images shows that v2.10.20 already contains the
getSecret()fix. The last vulnerable official Docker image we could identify is v2.10.10, which still usesgetPwd(). The reproduction script therefore uses v2.10.10 as the vulnerable baseline and v2.10.21 as the fixed baseline to ensure the vulnerability is actually demonstrated at runtime. - Environment: Docker 27.x, Ubuntu 22.04 sandbox, images pulled from
registry.cn-qingdao.aliyuncs.com/dataease/.
Variant Analysis & Alternative Triggers for CVE-2026-23958
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.doFilterinside theelsebranch (loginServerpresent):getPwd()→getSecret(). - What the fix assumes: All production deployments include the proprietary xpack JARs (
xpack-base.jar,xpack-permission.jar,xpack-sync.jar), so theloginServerbean is always present and theelsebranch is always taken. - What the fix does NOT cover:
- The
ifbranch inCommunityTokenFilter(loginServerabsent). SubstituleLoginServer.localLogin, which still signs JWTs withMd5Utils.md5(pwd).SubstituleLoginConfig.getPwd(), which still defaults toDataEase@123456.
- The
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:
- Attacker computes
secret = MD5("DataEase@123456"). - Attacker forges a JWT with claims
{uid: 1, oid: 1}signed withHS256(secret). - Attacker sends the forged JWT in the
X-DE-TOKENheader to an endpoint such as/de2api/menu/query. TokenFilterdecodes the JWT (without signature verification) and extractsuid/oid.CommunityTokenFilter.doFilterreaches theifbranch becauseCommonBeanFactory.getBean("loginServer")is empty.- The filter derives
secret = Md5Utils.md5(SubstituleLoginConfig.getPwd()), which equals the attacker's secret, so the JWT passes verification. - The request proceeds as the admin user.
- Attacker computes
Package/Component:
io.dataease.auth.filter.CommunityTokenFilter(sdk/common) andio.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:
// 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:
// 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
- Run
vuln_variant/reproduction_steps.sh. - The script builds a
dataease:v2.10.21-no-xpackimage by removing the proprietary xpack JARs from the official v2.10.21 Docker image. - It starts a MySQL container and the no-xpack DataEase container.
- It sends a forged JWT (signed with
MD5("DataEase@123456")) to/de2api/menu/query. - Expected result: HTTP 200 with actual menu data (bypass confirmed).
- It then starts the regular v2.10.21 DataEase (with xpack JARs) and repeats the request.
- 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
- Extend the fix to the community fallback path: In
CommunityTokenFilter.doFilter, replaceMd5Utils.md5(SubstituleLoginConfig.getPwd())with a per-instance random secret (e.g., generated on first startup and persisted insubstitule.json). - Synchronize
SubstituleLoginServersigning secret: EnsureSubstituleLoginServer.generate()uses the same per-instance random secret, notMd5Utils.md5(pwd). - 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.
- Add a startup secret generation step: If
substitule.jsondoes 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.
CVE-2026-23958 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Unknown error
Unknown error
git clone --depth=50 https://github.com/dataease/dataease.git /tmp/dataease && cd /tmp/dataease && git log --oneline --all | head -30{"cwd":"/root/.pruva/runs/cve-2026-23958_20260525-214431","duration_ms":5886,"exit_code":0,"stderr":"Cloning into '/tmp/dataease'...\nUpdating files: 35% (2296/6378)\rUpdating files: 36% (2297/6378)\rUpdating files: 37% (2360/6378)\rUpdating files: 38% (2424/6378)\rUpdating files: 39% (2488/6378)\rUpdating files: 40% (2552/6378)\rUpdating files: 41% (2615/6378)\rUpdating files: 42% (2679/6378)\rUpdating files: 43% (2743/6378)\rUpdating files: 44% (2807/6378)\rUpdating files: 45% (2871/6378)\rUpdating files: 46% (2934/6378)\rUpdating files: 47% (2998/6378)\rUpdating files: 48% (3062/6378)\rUpdating files: 49% (3126/6378)\rUpdating files: 50% (3189/6378)\rUpdating files: 51% (3253/6378)\rUpdating files: 52% (3317/6378)\rUpdating files: 53% (3381/6378)\rUpdating files: 54% (3445/6378)\rUpdating files: 55% (3508/6378)\rUpdating files: 56% (3572/6378)\rUpdating files: 57% (3636/6378)\rUpdating files: 58% (3700/6378)\rUpdating files: 59% (3764/6378)\rUpdating files: 60% (3827/6378)\rUpdating files: 61% (3891/6378)\rUpdating files: 62% (3955/6378)\rUpdating files: 63% (4019/6378)\rUpdating files: 64% (4082/6378)\rUpdating files: 65% (4146/6378)\rUpdating fil… [truncated]Artifacts and Evidence for CVE-2026-23958
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-23958
Upgrade dataease · github to v2.10.21 or later.
FAQ: CVE-2026-23958
How does the DataEase JWT forgery attack work?
Which DataEase versions are affected by CVE-2026-23958, and where is it fixed?
How severe is CVE-2026-23958?
How can I reproduce CVE-2026-23958?
References for CVE-2026-23958
Authoritative sources for CVE-2026-23958 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.