| 标题 | Chat2DB Chat2DB community server (chat2db-community) audited at commit `2905d33a2` (git describe `v5.3.6-109-g2905d33a2`); official source build; the service passes the matched table name into the pinned `com.baomidou:mybatis-plus-generator:3.5.3.1` path- |
|---|
| 描述 | Class-generator output files escape the declared exportPath through a traversal-named table in Chat2DB
Vendor: Chat2DB
Affected versions: audited at commit `2905d33a2` (git describe `v5.3.6-109-g2905d33a2`); official source build; the service passes the matched table name into the pinned `com.baomidou:mybatis-plus-generator:x.x.x.x` path-info/name-strategy flow at this commit
Vulnerability class: Path traversal leading to file write outside a declared boundary (CWE-22)
Preconditions: Anonymous in a headless/server deployment (`/api/**` unauthenticated; every Chat2DB request in the proof succeeded with no credentials). The caller must control a datasource of a type that accepts `../` inside quoted identifiers (PostgreSQL used in the proof), creatable anonymously via `POST /api/connection/datasource/create`; that datasource must contain a table literally named with the traversal segment (the generator emits files only for metadata-matched tables); and, for full file emission, the escape-side package directories (`com/my/entity`, `com/my/mapper`) must exist in the writable escape location — an attacker-controlled location in the practical attack. Without them the first unattended request still creates the escape directory tree outside `exportPath` but the file writes fail; with them present the request succeeds and all files land outside `exportPath`.
An anonymous caller of a headless Chat2DB deployment registers their own PostgreSQL datasource containing a quoted table name with `../` segments, and the class generator writes its entity/mapper/XML files outside the `exportPath` declared in the request — the traversal string survives into the output file names and the generated mapper namespace unescaped.
Data flow: JSON `tableName` at `POST /api/rdb/table/generate/class` → `DbMybatisGenerateServiceImpl.generateClass` (`:48` escaped path into `pathInfo[OutputFile.xml]`, `:119` `globalConfig.outputDir`, `:125` `addInclude` of the matched table) → `mybatis-plus-generator` `PathInfoHandler` / `NamingStrategy.underlineToCamel` (splits only on `_`; dots and slashes survive) → file writes outside `exportPath`.
### Affected interfaces
| Interface |
|---|
| POST /api/rdb/table/generate/class — JSON body `{"dataSourceId":<id>,"tableName":"../<name>","exportPath":"/tmp/out/jail"}`; anonymous in headless deployments |
| POST /api/connection/datasource/create — enabling interface (attacker-supplied PostgreSQL); anonymous in headless deployments |
Primary route detail:
```
POST /api/rdb/table/generate/class HTTP/1.1
Content-Type: application/json
{"dataSourceId":1791449467217999,"tableName":"../c2db-pwn-f2","exportPath":"/tmp/c2db-f2-out/jail"}
```
Anonymous (headless deployment): no cookie, token or `Authorization` header is required. The
datasource-existence guard is satisfied by the datasource the same caller just created.
### Root cause
1. `DbTableController.java:317-318` passes `tableName` verbatim to `DbMybatisGenerateServiceImpl.generateClass`; the service builds the output directory from it (`new File(exportPath, tableName)`) and, after the escape, hands the matched table name to the generator (`addInclude`/`table2Class`, `DbMybatisGenerateServiceImpl.java:48-59,113-133`).
2. `:48` reassigns `exportPath` to the escaped directory path; `:52` puts it as `pathInfo[OutputFile.xml]`; `:119` uses it as `globalConfig.outputDir`; `:120-124` set the package config (`parent=com.my`, `entity`, `mapper`, `xml(outputDir)`); `:125` `addInclude`; `:132` Freemarker engine.
3. Library internals on the pinned `mybatis-plus-generator:x.x.x.x` jar: `PathInfoHandler` computes output-file defaults as `joinPath(outputDir, packageInfo)` then overlays the application map; `NamingStrategy.underlineToCamel` splits only on `_` and upper-cases the next character — dots and slashes survive, so a matched table named `../c2db-pwn-f2` yields file names still containing the `../` prefix.
Missing guard: a database table identifier is data that the generator turns into class names and
paths; nothing validates it as a plain identifier, and nothing canonicalizes the computed output
file paths and asserts containment inside the declared `exportPath`.
Baseline: official source build at commit `2905d33a2`, headless server self-hosted on `127.0.0.1:18762`; attacker-side PostgreSQL 16 container on `127.0.0.1:18962` started by the reporter on the same host. No credentials were sent to Chat2DB anywhere in this proof.
**1. Plant the traversal-named table (quoted identifier, legal PostgreSQL):**
```sql
CREATE TABLE "../c2db-pwn-f2" (id int);
CREATE TABLE orders (id int, name text);
```
**2. Register the attacker datasource anonymously** (`POST /api/connection/datasource/create`, `url:"jdbc:postgresql://127.0.0.1:18962/postgres"`) → `{"success":true,...,"data":{"id":1791449467217999,...}}`.
**3. Positive — generate with the traversal-named table:**
```sh
$ curl -X POST 'http://127.0.0.1:18762/api/rdb/table/generate/class' -H 'Content-Type: application/json' \
-d '{"dataSourceId":1791449467217999,"tableName":"../c2db-pwn-f2","exportPath":"/tmp/c2db-f2-out/jail"}'
{"success":true,"errorCode":null,"errorMessage":null,...} # HTTP 200
```
The declared `exportPath` stayed empty and every generated artifact landed outside it:
```
/tmp/c2db-f2-out/jail <- declared exportPath, empty
/tmp/c2db-f2-out/c2db-pwn-f2/com/my/c2db-pwn-f2DO.java (404B, entity)
/tmp/c2db-f2-out/c2db-pwn-f2/com/my/c2db-pwn-f2Mapper.java (284B, mapper)
/tmp/c2db-f2-out/c2db-pwn-f2Mapper.xml (214B, xml; one level further out)
```
The mapper XML contains `<mapper namespace="com.my.mapper.../c2db-pwn-f2Mapper">` — the original `../` segments unescaped.
**4. Negative control — ordinary table name keeps the boundary:** same datasource, `tableName:"orders"`, `exportPath:"/tmp/c2db-f2-neg/jail2"` → `success:true`, all artifacts strictly inside the declared root (`.../jail2/orders/com/my/entity/OrdersDO.java` etc.); no escape anywhere under `/tmp`. The single variable between positive and negative is the table name.
**5. Stated conditions, measured at runtime:** with a two-level `../../` name and a bare `/tmp` `exportPath`, the escape resolves to a non-writable root location for a non-root server user (generator reports `Unable to create directory`, no files land) — the depth must target a writable location; when the escape-side package directories do not exist, the first unattended request fails at `FileNotFoundException` on the entity write but has already created the escape directory tree outside `exportPath`; pre-creating `com/my/entity|mapper` in the attacker-writable escape location turns the repeat into the full `success:true` file-level escape.
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 - chat2db/Chat2DB
- **Status**: vendor notified 2026-10-09 through GitHub Private Vulnerability Reporting; private report <<GHSA_LINK>>; 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/chat2db/Chat2DB |
|---|
| 用户 | lucasg2g (UID 84737) |
|---|
| 提交 | 2026-10-15 07時34分 (24 日前) |
|---|
| 管理 | 2026-11-08 15時18分 (24 days later) |
|---|
| 状态 | 已接受 |
|---|
| VulDB条目 | 432889 [OtterMind Chat2DB 直到 5.3.8 Class Generator DbMybatisGenerateServiceImpl.java DbMybatisGenerateServiceImpl.generateClass tableName 目录遍历] |
|---|
| 积分 | 20 |
|---|