# CVE-2026-110123: maku-boot returns SMTP and cloud SMS credentials to any authenticated user through notification-config endpoints that skip the authority check
, mail and SMS notification configuration module
**Affected:** v5.0.0 release, reviewed at tag commit `4cd202b037e2ac47e366d4b0691fb387c764d308` (released 2026-04-28). The same missing-authority pattern is present in the prior Spring Security 6 line, confirmed at v4.11.0 commit `f9dd43c8913d5acbb46d061e4a9fc47c42f3034f`.
**Class:** CWE-862 Missing Authorization, CWE-522 Insufficiently Protected Credentials, CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
**Write-up:**
maku-boot protects its notification configuration module with Spring Security method annotations. Every management action on a mail platform or an SMS platform carries `@PreAuthorize("hasAuthority('sys:mail:config')")` or `@PreAuthorize("hasAuthority('sys:sms:config')")`, except three handlers that were left without any authority check. `GET /sys/mail/config/list` (`SysMailConfigController.java:53-59`) and `GET /sys/sms/config/list` (`SysSmsConfigController.java:53-59`) have no `@PreAuthorize`, and the service behind them runs a plain `selectList` with no data-scope and no field masking (`SysMailConfigServiceImpl.java:40-47`, `SysSmsConfigServiceImpl.java:42-48`). The returned view objects carry the SMTP sender password and the cloud `accessKey` and `secretKey` in plaintext (`SysMailConfigVO.java:40,49,52`, `SysSmsConfigVO.java:47,50`). The only runtime authorization in the application is the global filter chain rule `anyRequest().authenticated()` plus these per-method annotations (`SecurityFilterConfig.java:43-46`), so any principal that holds a valid token, including a low-privilege account that was never granted `sys:mail:config` or `sys:sms:config`, can read every stored mail and SMS platform credential. A fourth handler, `POST /sys/mail/config/send` (`SysMailConfigController.java:79-82`), is also missing the authority check, while its SMS counterpart `POST /sys/sms/config/send` keeps it, so the same low-privilege account can also send mail through any stored platform. The trust boundary crossed is the function-level permission model that the product itself declares for this module.
The product enforces function-level authorization in exactly one way at runtime, the Spring Security `@PreAuthorize` annotation on each controller method. The notification-config controllers apply that annotation to the management actions but omit it on the list read and on the mail send. Because the global rule only demands that the request be authenticated, an omitted annotation means the handler is reachable by every authenticated principal regardless of granted authorities. Five source facts at the pinned commit establish the defect.
### 1. The only runtime authorization layers are the permit list and `@PreAuthorize`
`maku-framework/src/main/java/net/maku/framework/security/config/SecurityFilterConfig.java:40-51` builds the single `SecurityFilterChain`. It permits the URLs listed in `auth.yml`, permits `OPTIONS`, and requires authentication for everything else. There is no URL-to-permission map and no custom `AuthorizationManager`.
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.requestMatchers(HttpMethod.OPTIONS).permitAll()
The token filter `AuthenticationTokenFilter.doFilterInternal` (`maku-framework/src/main/java/net/maku/framework/security/filter/AuthenticationTokenFilter.java:42-54`) resolves the `Authorization` token to a cached `UserDetail` and installs an `Authentication` carrying that user's own granted authorities. A valid token for any account, with any set of permissions, passes the `anyRequest().authenticated()` gate. The per-authority decision therefore happens only where a `@PreAuthorize` annotation exists. The helper `PreAuthorizeUtil.getPreAuthorizeList` scans `@PreAuthorize` values to build the list of selectable permission strings in the admin UI, it does not enforce anything at request time.
### 2. The mail config list handler has no authority check
`maku-boot-system/src/main/java/net/maku/system/controller/SysMailConfigController.java:44-59` shows the paged read guarded by `sys:mail:config`, and directly below it the `list` read with no annotation at all.
@PreAuthorize("hasAuthority('sys:mail:config')")
public Result > page(@ParameterObject @Valid SysMailConfigQuery query) {
PageResult page = sysMailConfigService.page(query);
public Result > list(Integer platform) {
List list = sysMailConfigService.list(platform);
The `{id}` info read, the `save`, the `update`, and the `delete` on the same controller all carry `@PreAuthorize("hasAuthority('sys:mail:config')")`. The `list` handler is the outlier. The path `/sys/mail/config/list` is not in any `auth.yml` ignore list, so it is not permitAll, it is reachable by any authenticated caller.
### 3. The service returns unmasked entities including the stored secrets
`maku-boot-system/src/main/java/net/maku/system/service/impl/SysMailConfigServiceImpl.java:40-47` runs a plain MyBatis-Plus `selectList` filtered only by the optional `platform` query parameter. There is no data-scope restriction, no owner or organization filter, and no status filter.
public List list(Integer platform) {
LambdaQueryWrapper wrapper = Wrappers.lambdaQuery();
wrapper.eq(platform != null, SysMailConfigEntity::getPlatform, platform);
List list = baseMapper.selectList(wrapper);
return BeanUtil.copyToList(list, SysMailConfigVO.class);
`BeanUtil.copyToList` copies every field into `SysMailConfigVO`, and that view object declares the sender password and the cloud keys as plain strings (`maku-boot-system/src/main/java/net/maku/system/vo/SysMailConfigVO.java:40,49,52`).
Those values come straight from the `sys_mail_config` table columns on `SysMailConfigEntity` and are never masked on the read path. For the local SMTP platform `mailPass` is the sender mailbox password. For the Aliyun platform `accessKey` and `secretKey` are the cloud account keys.
### 4. The SMS config list handler repeats the pattern
`maku-boot-system/src/main/java/net/maku/system/controller/SysSmsConfigController.java:44-59` shows the same shape. The paged read is guarded by `sys:sms:config`, the `list` read is not.
@PreAuthorize("hasAuthority('sys:sms:config')")
public Result > page(@ParameterObject @Valid SysSmsConfigQuery query) { ... }
public Result > list(Integer platform) {
List list = sysSmsConfigService.list(platform);
`SysSmsConfigServiceImpl.list` (`maku-boot-system/src/main/java/net/maku/system/service/impl/SysSmsConfigServiceImpl.java:42-48`) is again a plain `selectList`, and `SysSmsConfigVO` carries the SMS provider `accessKey` and `secretKey` as plain strings (`maku-boot-system/src/main/java/net/maku/system/vo/SysSmsConfigVO.java:47,50`). These are the Aliyun, Tencent Cloud, Qiniu, or Huawei Cloud SMS account keys, depending on the configured platform.
### 5. The mail send handler is also missing the authority check, unlike the SMS send
`maku-boot-system/src/main/java/net/maku/system/controller/SysMailConfigController.java:79-84` performs an outbound email send. It has no `@PreAuthorize`.
@OperateLog(type = OperateTypeEnum.OTHER)
public Result send(@RequestBody SysMailSendVO vo) {
SysMailConfigEntity entity = sysMailConfigService.getById(vo.getId());
EmailConfig config = BeanUtil.copyProperties(entity, EmailConfig.class);
The handler loads a stored platform by the caller-supplied `vo.getId()`, copies it into an `EmailConfig`, and sends mail to caller-supplied recipients with caller-supplied subject and body. The analogous SMS send on the other controller keeps its guard, `SysSmsConfigController.java:80-84` carries `@PreAuthorize("hasAuthority('sys:sms:config')")`. The asymmetry confirms the missing annotation on the mail send is an oversight, not a deliberate open endpoint.
participant A as Low privilege authenticated user
participant F as Spring Security filter chain
participant MC as SysMailConfigController list handler
participant SV as SysMailConfigServiceImpl
A->>F: GET /sys/mail/config/list with a valid token
F->>F: token resolves to a user and the request is authenticated
F->>MC: no method authority is required on the list handler
MC->>SV: call list with the platform parameter
SV->>DB: selectList filtered only by platform
DB-->>SV: all rows including SMTP password and cloud keys
SV-->>MC: view object list with mailPass accessKey secretKey in plaintext
MC-->>A: JSON body containing the credentials
The following requests are illustrative and were not executed against any live deployment. They describe the reachable behavior derived from the pinned source.
Prerequisites. The attacker holds a valid session token for any maku-boot account. maku-boot is an administrative back-office platform, and a realistic account is a low-privilege operator or sub-account that was granted some permissions but not the mail or SMS configuration permission. The attacker does not need the `sys:mail:config` or `sys:sms:config` authority. No pre-authentication path is claimed.
1. Obtain a token. The attacker authenticates through the normal login endpoint `POST /sys/auth/login` with the low-privilege credentials and keeps the returned `accessToken`. The token is sent either in the `Authorization` header or as the `access_token` request parameter, both are accepted by `TokenUtils.getAccessToken`.
2. Read every mail platform credential. Send `GET /sys/mail/config/list` with the token. The optional `platform` parameter can be omitted to return all platforms, or set to `-1` for local SMTP and `0` for Aliyun. The response body is a JSON array where each element contains `mailHost`, `mailPort`, `mailFrom`, `mailPass`, `regionId`, `endpoint`, `accessKey`, and `secretKey`. For a local SMTP platform the attacker now holds the sender mailbox password. For an Aliyun platform the attacker holds the cloud `accessKey` and `secretKey`.
3. Read every SMS platform credential. Send `GET /sys/sms/config/list` with the token, optionally with a `platform` value for Aliyun, Tencent Cloud, Qiniu, or Huawei Cloud. Each element in the response contains `signName`, `templateId`, `appId`, `senderId`, `url`, `accessKey`, and `secretKey`. The attacker now holds the SMS provider account keys.
4. Optionally send mail through a stored platform. Send `POST /sys/mail/config/send` with the token and a JSON body that sets `id` to a mail platform identifier learned in step 2, `platform` to that platform type, `mailTos` to arbitrary recipients, and `subject` and `content` to arbitrary values. The server sends the message using the stored platform, so the mail originates from the organization sender address without the attacker holding the mail configuration permission.
This finding was verified by static source review of the pinned v5.0.0 tag commit `4cd202b037e2ac47e366d4b0691fb387c764d308` through read-only GitHub source pages. The verification covered the complete chain. The single `SecurityFilterChain` and its `anyRequest().authenticated()` rule were confirmed in `SecurityFilterConfig.java`. The absence of any URL-to-permission interceptor or custom `AuthorizationManager` was confirmed by enumerating the framework config and interceptor packages, the only MyBatis interceptor is `DataScopeInnerInterceptor` which rewrites SQL for data-scope-annotated queries and does not apply to the plain `selectList` used here, and the only aspect is `OperateLogAspect` which writes audit logs. The missing `@PreAuthorize` on the two `list` handlers and the mail `send` handler, and its presence on every sibling handler and on the SMS `send`, were read directly in the two controllers. The plaintext credential fields were read in the two view objects. The cross-version check confirmed the identical pattern at v4.11.0 commit `f9dd43c8913d5acbb46d061e4a9fc47c42f3034f`, which places the defect in the current line and the immediate predecessor rather than a single regression.
Runtime validation was not performed. No request was sent to any live deployment, no account or real data was used, and no proof-of-concept was executed. The step where the leaked keys are replayed against the external cloud provider, and the step where the send handler delivers a message, are inferred from the source path and the documented behavior of the referenced libraries, not observed.
Credential disclosure, mail. Any authenticated principal can read `GET /sys/mail/config/list` and obtain the stored SMTP sender password or the Aliyun mail `accessKey` and `secretKey` in plaintext. A leaked SMTP password allows sending mail as the organization and reading or relaying through that mailbox depending on the provider. A leaked Aliyun key pair is credential material for the organization cloud account and its blast radius depends on the policy attached to that key.
Credential disclosure, SMS. Any authenticated principal can read `GET /sys/sms/config/list` and obtain the SMS provider `accessKey` and `secretKey` for Aliyun, Tencent Cloud, Qiniu, or Huawei Cloud. These keys allow sending SMS and incurring cost on the organization account, and their broader reach depends on the permissions attached to the key.
Unauthorized mail send. Any authenticated principal can call `POST /sys/mail/config/send` and send mail to arbitrary recipients through any stored platform, which supports phishing or spam that originates from the trusted organization sender domain.
What is NOT established. This is not a pre-authentication issue, a valid token for some account is required. There is no remote code execution, no SQL injection, and no authentication bypass in this finding. The read returns whatever secrets the operator stored, if a platform row was never configured there is nothing to leak. The real-world severity of the leaked cloud keys depends entirely on the privileges the operator assigned to those keys at the provider, which is outside this codebase. The headline rating is Medium, CVSS 3.1 vector AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N, base score 6.5, reflecting an authenticated low-privilege read of high-confidentiality secrets. If the leaked keys grant control of a separate cloud account the practical impact is higher than the in-application base score, and the mail send adds an integrity and abuse dimension that this confidentiality-focused vector does not score.
**Where exactly:** `maku-boot-system/src/main/java/net/maku/system/controller/SysMailConfigController.java:53-59` and `:79-82`, and `maku-boot-system/src/main/java/net/maku/system/controller/SysSmsConfigController.java:53-59`.
+ @PreAuthorize("hasAuthority('sys:mail:config')")
public Result > list(Integer platform) {
List list = sysMailConfigService.list(platform);
@OperateLog(type = OperateTypeEnum.OTHER)
+ @PreAuthorize("hasAuthority('sys:mail:config')")
public Result send(@RequestBody SysMailSendVO vo) {
+ @PreAuthorize("hasAuthority('sys:sms:config')")
public Result > list(Integer platform) {
List list = sysSmsConfigService.list(platform);
Short term. Add the matching `@PreAuthorize` authority to the two `list` handlers and to the mail `send` handler so they require the same `sys:mail:config` or `sys:sms:config` authority as their siblings.
Medium term. Stop returning the secret fields on any read handler that a non-owner can reach. Mask or omit `mailPass`, `accessKey`, and `secretKey` in the read view objects, and accept them only on write. A dropdown list that the UI uses to pick a platform needs the id, name, and platform type, not the key material.
Defense in depth. Replace the implicit permit-by-omission model with a deny-by-default authorization layer, for example a request-level rule that maps each controller path to its required authority, so a handler that forgets its annotation fails closed instead of becoming reachable by every authenticated user.
The minimal correct fix is adding `@PreAuthorize` to the three unguarded handlers so they require the same authority as the rest of their controller. The durable fix is to never serialize stored credentials into a read response and to make the authorization layer fail closed on a missing annotation.
- No public advisory exists for this finding at time of writing. No CVE or GHSA covers the maku-boot notification-config authorization gap, and the repository has no published security advisory.
- The only prior maku-boot advisory is GHSA-3qq9-74gh-x46m / CVE-2022-4322, a scheduled-task handler injection in `AbstractScheduleJob.doExecute` affecting versions up to 2.2.0. That is a different component and is already mitigated in v5.0.0 by the `@Service` bean allowlist in `ScheduleJobController.checkBean`. This finding is unrelated.
- Pinned source, single security filter chain:
- Pinned source, token filter installs the principal authorities:
- Pinned source, mail config controller list and send without authority:
- Pinned source, SMS config controller list without authority:
- Pinned source, mail config service list returns unmasked entities:
- Pinned source, SMS config service list returns unmasked entities:
- Pinned source, mail config view object with plaintext secrets:
- Pinned source, SMS config view object with plaintext secrets:
- Prior unrelated advisory:
Reported by hieuPenguinnn, [github.com/hieuPenguinnn](
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
