{"repro_id":"REPRO-2026-00168","version":7,"title":"DataEase: authentication bypass via password-derived HMAC JWT signing key","repro_type":"security","status":"published","severity":"critical","cvss_score":9.8,"description":"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`.\n\nAn **unauthenticated, anonymous attacker** with only network access can therefore:\n\n1. Compute `secret = MD5(\"DataEase@123456\")`.\n2. Forge a JWT with header `{alg: HS256, typ: JWT}` and payload `{uid: 1, oid: 1}`.\n3. Present it as the `X-DE-TOKEN` (DataEase's auth header) and call any authenticated REST endpoint as the admin user.\n\nThis 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.","root_cause":"# RCA Report — CVE-2026-23958 (DataEase JWT Authentication Bypass)\n\n## Summary\n\nDataEase 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.\n\n## Impact\n\n- **Package/Component**: `io.dataease.auth.filter.CommunityTokenFilter` (sdk/common) and `io.dataease.xpack.permissions.login.bo.LoginUserCacheBO` (xpack-permission)\n- **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.\n- **Risk level**: High — unauthenticated remote attacker can impersonate the admin user.\n- **Consequences**: Full admin takeover via forged JWT, enabling subsequent exploitation of authenticated endpoints.\n\n## Root Cause\n\nIn 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:\n\n```java\nObject apisixCacheManage = CommonBeanFactory.getBean(\"apisixCacheManage\");\nMethod method = DeReflectUtil.findMethod(apisixCacheManage.getClass(), \"userCacheBO\");\nObject o = ReflectionUtils.invokeMethod(method, apisixCacheManage, userId);\nMethod pwdMethod = DeReflectUtil.findMethod(o.getClass(), \"getPwd\");  // vulnerable\nObject pwdObj = ReflectionUtils.invokeMethod(pwdMethod, o);\nsecret = pwdObj.toString();\n```\n\nIn 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.\n\nFix 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+.\n\n## Reproduction Steps\n\n1. Run `repro/reproduction_steps.sh`\n2. The script:\n   - Starts a MySQL 8 container and a DataEase **v2.10.10** container (vulnerable)\n   - Waits for the API to respond on `http://127.0.0.1:8100`\n   - Baselines an anonymous request to `/de2api/user/personInfo` → expects **401**\n   - Forges a JWT with `secret = MD5(\"DataEase@123456\")` and sends it as `X-DE-TOKEN` → expects **200**\n   - Stops the vulnerable app but **preserves the MySQL data**\n   - Starts a DataEase **v2.10.21** container against the **same** MySQL data\n   - Replays the **identical** forged JWT → expects **401** with `DE-GATEWAY-FLAG` header\n3. Expected evidence:\n   - `logs/vulnerable_attack_response.txt` shows HTTP 200\n   - `logs/fixed_attack_response.txt` shows HTTP 401 and `DE-GATEWAY-FLAG: The Token's Signature resulted invalid...`\n\n## Evidence\n\n- `logs/repro_run1.log` — first successful execution of `reproduction_steps.sh`\n- `logs/repro_run2.log` — second successful execution (idempotency confirmed)\n- `logs/vulnerable_transcript.txt` — summary of v2.10.10 test results\n- `logs/fixed_transcript.txt` — summary of v2.10.21 test results\n- `logs/vulnerable_attack_response.txt` — raw HTTP response showing 200 on forged JWT\n- `logs/fixed_attack_response.txt` — raw HTTP response showing 401 + `DE-GATEWAY-FLAG`\n\nKey excerpts from v2.10.10 (vulnerable):\n```\nHTTP/1.1 200\nX-DE-EXECUTE-VERSION: 2.10.10\n...\n{\"code\":60003,\"msg\":\"缺少许可证\",\"data\":null}\n```\n(The 200 status proves the JWT signature was accepted; the downstream \"missing license\" error is irrelevant to the auth bypass.)\n\nKey excerpts from v2.10.21 (fixed):\n```\nHTTP/1.1 401\nX-DE-EXECUTE-VERSION: 2.10.21\nDE-GATEWAY-FLAG: The%20Token%27s%20Signature%20resulted%20invalid%20when%20verified%20using%20the%20Algorithm%3A%20HmacSHA256\n```\n\n## Recommendations / Next Steps\n\n1. **Upgrade** to DataEase v2.10.20 or later. The fix commit is already present in those builds.\n2. **Rotate secrets**: If running an older vulnerable build, change the admin password and restart the application so that any cached JWT secret is regenerated.\n3. **Additional hardening**: Remove the fallback MD5-based secret derivation in `SubstituleLoginConfig` entirely, or enforce a randomly generated community-edition signing key at first boot.\n4. **Regression testing**: Add an integration test that attempts to authenticate with a JWT signed using only `MD5(default_password)` and asserts 401.\n\n## Additional Notes\n\n- **Idempotency**: `reproduction_steps.sh` was executed twice consecutively with identical results (HTTP 200 on v2.10.10, HTTP 401 on v2.10.21).\n- **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 uses `getPwd()`. 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.\n- **Environment**: Docker 27.x, Ubuntu 22.04 sandbox, images pulled from `registry.cn-qingdao.aliyuncs.com/dataease/`.\n","cve_id":"CVE-2026-23958","cwe_id":"CWE-522","source_url":"https://github.com/dataease/dataease","package":{"name":"dataease","ecosystem":"github","affected_versions":"<= v2.10.20","fixed_version":"v2.10.21"},"reproduced_at":"2026-05-25T23:25:04.719537+00:00","duration_secs":6011.321615695953,"tool_calls":630,"turns":605,"handoffs":3,"total_cost_usd":9.695817239999986,"agent_costs":{"repro":8.108909229999997,"support":0.02131377,"vuln_variant":1.56559424},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p6":8.108909229999997},"support":{"accounts/fireworks/models/kimi-k2p6":0.02131377},"vuln_variant":{"accounts/fireworks/models/kimi-k2p6":1.56559424}},"vulnerable_version_variant_outcome":"unknown","fix_bypass_outcome":"unknown","variant_disclosure_state":"unknown","quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-05-25T23:25:35.388475+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":5758,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":7300,"category":"reproduction_script"},{"path":"bundle/context.json","filename":"context.json","size":6997,"category":"other"},{"path":"bundle/metadata.json","filename":"metadata.json","size":836,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":5912,"category":"ticket"},{"path":"bundle/repro/my.cnf","filename":"my.cnf","size":617,"category":"other"},{"path":"bundle/repro/mysql-init/01-init.sql","filename":"01-init.sql","size":97,"category":"other"},{"path":"bundle/repro/patch_analysis.md","filename":"patch_analysis.md","size":3798,"category":"documentation"},{"path":"bundle/repro/application.yml","filename":"application.yml","size":881,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1443,"category":"other"},{"path":"bundle/repro/docker-compose-fix.yml","filename":"docker-compose-fix.yml","size":1253,"category":"other"},{"path":"bundle/repro/docker-compose-vuln.yml","filename":"docker-compose-vuln.yml","size":1253,"category":"other"},{"path":"bundle/logs/v2.10.21_forged_status.txt","filename":"v2.10.21_forged_status.txt","size":3,"category":"other"},{"path":"bundle/logs/full_bypass_headers.txt","filename":"full_bypass_headers.txt","size":232,"category":"other"},{"path":"bundle/logs/v2.10.18_forged_status.txt","filename":"v2.10.18_forged_status.txt","size":3,"category":"other"},{"path":"bundle/logs/v2.10.21_baseline_status.txt","filename":"v2.10.21_baseline_status.txt","size":3,"category":"other"},{"path":"bundle/logs/fixed_transcript.txt","filename":"fixed_transcript.txt","size":285,"category":"other"},{"path":"bundle/logs/v2.10.18_baseline_headers.txt","filename":"v2.10.18_baseline_headers.txt","size":194,"category":"other"},{"path":"bundle/logs/v2.10.18_baseline_status.txt","filename":"v2.10.18_baseline_status.txt","size":3,"category":"other"},{"path":"bundle/logs/vulnerable_transcript.txt","filename":"vulnerable_transcript.txt","size":285,"category":"other"},{"path":"bundle/logs/no_xpack_baseline_headers.txt","filename":"no_xpack_baseline_headers.txt","size":189,"category":"other"},{"path":"bundle/logs/no_xpack_baseline_body.txt","filename":"no_xpack_baseline_body.txt","size":76,"category":"other"},{"path":"bundle/logs/fixed_attack_response.txt","filename":"fixed_attack_response.txt","size":317,"category":"other"},{"path":"bundle/logs/full_bypass_body.txt","filename":"full_bypass_body.txt","size":84,"category":"other"},{"path":"bundle/logs/no_xpack_bypass_body.txt","filename":"no_xpack_bypass_body.txt","size":2805,"category":"other"},{"path":"bundle/logs/no_xpack_bypass_headers.txt","filename":"no_xpack_bypass_headers.txt","size":306,"category":"other"},{"path":"bundle/logs/docker_build_no_xpack.log","filename":"docker_build_no_xpack.log","size":1471,"category":"log"},{"path":"bundle/logs/variant_test.log","filename":"variant_test.log","size":10278,"category":"log"},{"path":"bundle/logs/evidence_fixed_headers.txt","filename":"evidence_fixed_headers.txt","size":232,"category":"other"},{"path":"bundle/logs/v2.10.21_baseline_headers.txt","filename":"v2.10.21_baseline_headers.txt","size":194,"category":"other"},{"path":"bundle/logs/repro_run1.log","filename":"repro_run1.log","size":1171,"category":"log"},{"path":"bundle/logs/full_baseline_body.txt","filename":"full_baseline_body.txt","size":76,"category":"other"},{"path":"bundle/logs/jwt_details.txt","filename":"jwt_details.txt","size":162,"category":"other"},{"path":"bundle/logs/vulnerable_attack_response.txt","filename":"vulnerable_attack_response.txt","size":363,"category":"other"},{"path":"bundle/logs/v2.10.18_baseline_body.json","filename":"v2.10.18_baseline_body.json","size":81,"category":"other"},{"path":"bundle/logs/v2.10.18_forged_headers.txt","filename":"v2.10.18_forged_headers.txt","size":312,"category":"other"},{"path":"bundle/logs/v2.10.21_baseline_body.json","filename":"v2.10.21_baseline_body.json","size":81,"category":"other"},{"path":"bundle/logs/v2.10.18_forged_body.json","filename":"v2.10.18_forged_body.json","size":50,"category":"other"},{"path":"bundle/logs/repro_run2.log","filename":"repro_run2.log","size":1171,"category":"log"},{"path":"bundle/logs/v2.10.21_forged_headers.txt","filename":"v2.10.21_forged_headers.txt","size":232,"category":"other"},{"path":"bundle/logs/evidence_vulnerable_headers.txt","filename":"evidence_vulnerable_headers.txt","size":312,"category":"other"},{"path":"bundle/logs/full_baseline_headers.txt","filename":"full_baseline_headers.txt","size":189,"category":"other"},{"path":"bundle/logs/evidence_fixed_body.json","filename":"evidence_fixed_body.json","size":84,"category":"other"},{"path":"bundle/logs/evidence_vulnerable_body.json","filename":"evidence_vulnerable_body.json","size":50,"category":"other"},{"path":"bundle/logs/v2.10.21_forged_body.json","filename":"v2.10.21_forged_body.json","size":84,"category":"other"}]}