# CVE-2026-58377 Variant — JeecgBoot Broken Access Control (Alternate Trigger)

## Summary

The parent CVE-2026-58377 reproduction validated broken access control only on
`OpenApiAuthController` (`/openapi/auth/*`). This variant stage confirmed that the
**same root cause** — missing Shiro `@RequiresRoles`/`@RequiresPermissions`
annotations combined with a Shiro filter chain that maps `/**` to the
authentication-only `jwt` filter — is reachable through **two additional, materially
distinct entry points** that the parent reproduction never tested:

- **VARIANT A (primary, NEW surface not mentioned anywhere in the CVE description):**
  `OpenApiLogController` (`/openapi/record/*`) exposes full CRUD
  (`/list`, `/add`, `/edit`, `/delete`, `/deleteBatch`, `/queryById`) over the
  OpenAPI **call/audit log** table. A low-privileged authenticated user (role `test`)
  can read every audit log, **create** log rows attributed to another user's/admin's
  auth record, and **delete** log rows — i.e. audit-log disclosure, forgery, and
  tampering/destruction.
- **VARIANT B (corroborating; CVE-claimed surface that the repro stage did NOT actually exercise):**
  `OpenApiPermissionController` (`/openapi/permission/*`) exposes `POST /add` (grant
  OpenAPI permissions on **any** auth record, including the admin's) and
  `GET /getOpenApi` (retrieve the OpenAPI definitions mapped to any auth record).

Because **no official patch exists** (CVE status `Deferred`; the latest release tag is
`v3.9.2`, which is the vulnerable version itself; and `master` HEAD
`32a8dfe94f014f7a9d472bce83e08bb8cf9c10c3` is **byte-identical** to `v3.9.2` for all
four OpenAPI controllers), this is an **alternate trigger confirmed on the latest
available code**, not merely an old-version issue. The "fixed-version" check reduces to
testing the latest release / default branch, which is identical to the vulnerable code.

## Fix Coverage / Assumptions

There is **no fix/patch** for CVE-2026-58377 as of this analysis:

- NVD/cvefeed.io lists the CVE status as **Deferred**.
- SecAlerts lists **"No Patch"** for CVE-2026-58377.
- The highest release tag is `v3.9.2` (commit `7df07a823fd558be857d0208ccae96342539fbc1`),
  which is the vulnerable version itself.
- `master` HEAD (`32a8dfe94f014f7a9d472bce83e08bb8cf9c10c3`) was fetched and the four
  OpenAPI controller source files are **byte-identical** to the `v3.9.2` versions
  (verified by `diff` against the project-cache checkout).

The **hypothetical** fix the parent RCA recommends is: add `@RequiresRoles({"admin"})`
to all mutating endpoints in `OpenApiAuthController` and `OpenApiPermissionController`,
and verify the SK signature in `OpenApiController.call()`. The invariant that fix would
rely on is: *"every sensitive OpenAPI endpoint carries a Shiro authorization annotation,
so the `AuthorizationAttributeSourceAdvisor` enforces role checks before the method runs."*

**What that hypothetical fix would NOT cover (the gap this variant exposes):**

- `OpenApiLogController` (`/openapi/record/*`) — **not mentioned in the CVE description at
  all**, and not covered by the parent RCA's recommended annotations. It would remain
  fully unprotected, leaving audit-log CRUD exploitable by any authenticated low-priv user.
- The read endpoints `GET /openapi/list` and `GET /openapi/queryById` on `OpenApiController`
  also carry no `@RequiresRoles`, allowing any authenticated user to enumerate/read all
  OpenAPI definitions (including `originUrl`, `headersJson`, `paramsJson`).

## Variant / Alternate Trigger

### Variant A — `OpenApiLogController` (`/openapi/record/*`)  [NEW surface]

- **File:** `jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/controller/OpenApiLogController.java`
- **Entry points:** `GET /openapi/record/list`, `POST /openapi/record/add`,
  `PUT /openapi/record/edit`, `DELETE /openapi/record/delete`,
  `DELETE /openapi/record/deleteBatch`, `GET /openapi/record/queryById`
- **Code path:** Every method delegates directly to the MyBatis-Plus `ServiceImpl`
  (`service.save`/`service.updateById`/`service.removeById`/`service.page`) with **no**
  `@RequiresRoles`/`@RequiresPermissions` annotation. Shiro's `JwtFilter` authenticates
  the caller (validates the JWT) but performs **no authorization**; the
  `AuthorizationAttributeSourceAdvisor` is never triggered because there is no annotation
  to advise on.

### Variant B — `OpenApiPermissionController` (`/openapi/permission/*`)  [CVE-claimed, unreproduced]

- **File:** `jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/controller/OpenApiPermissionController.java`
- **Entry points:** `POST /openapi/permission/add`, `GET /openapi/permission/getOpenApi`
- **Code path:** `POST /add` calls `service.add(...)`, which deletes all existing
  `OpenApiPermission` rows for the supplied `apiAuthId` then inserts new rows for each
  comma-separated `apiId` — with no authorization check and **no validation that the
  caller owns `apiAuthId`**. A low-priv user can therefore grant OpenAPI permissions
  against the **admin's** auth record. `GET /getOpenApi` returns the full list of
  OpenAPI definitions (with per-API `ifCheckBox` flags) for any `apiAuthId`.

### Negative control (confirms the gap is annotation-specific)

- `POST /openapi/add` (`OpenApiController`) carries `@RequiresRoles({"admin"})` and
  correctly rejects the low-priv user with `Subject does not have role [admin]`. This
  proves the Shiro authorization layer is functional and that the vulnerability is
  specifically the **absence** of annotations on the controllers above.

## Impact

- **Package/component affected:**
  - `org.jeecg.modules.openapi.controller.OpenApiLogController` (Variant A — primary)
  - `org.jeecg.modules.openapi.controller.OpenApiPermissionController` (Variant B)
  - Same `jeecg-system-biz` module as the parent CVE.
- **Affected versions (as tested):** JeecgBoot `v3.9.2`
  (commit `7df07a823fd558be857d0208ccae96342539fbc1`). The latest `master` HEAD
  (`32a8dfe`) is byte-identical for these controllers, so the latest available code is
  also affected.
- **Risk level:** HIGH. Variant A adds a distinct impact class to the CVE: **audit-log
  tampering/destruction and audit-log disclosure** by any authenticated low-priv user,
  in addition to the credential CRUD the parent CVE describes. Variant B enables a
  low-priv user to grant themselves (or alter) OpenAPI permissions on any auth record
  and enumerate the OpenAPI surface.
- **Consequences:**
  - A low-priv user can read every OpenAPI call log (`GET /openapi/record/list`,
    `total=27` observed) regardless of ownership.
  - A low-priv user can **create** forged audit-log rows attributed to the admin's auth
    record (`POST /openapi/record/add` with `callAuthId` = admin's auth id; persisted and
    verified in the DB).
  - A low-priv user can **delete** audit-log rows (`DELETE /openapi/record/delete`;
    verified the row count drops from 1 → 0 in the DB), enabling cover-up of API call
    activity.
  - A low-priv user can grant OpenAPI permissions on the admin's auth record
    (`POST /openapi/permission/add`; 2 rows persisted against the admin auth id) and
    enumerate the OpenAPI definitions mapped to it (`GET /openapi/permission/getOpenApi`).

## Impact Parity

- **Disclosed/claimed maximum impact (parent CVE):** Privilege escalation — low-priv
  users perform full CRUD on OpenAPI AK/SK credentials and can invoke the OpenAPI
  surface as the credential owner.
- **Reproduced impact from this variant run:**
  - Variant A: low-priv user performed **full CRUD on the OpenAPI audit-log table**
    (read 27 logs, create a forged log attributed to admin's auth id, delete a log) — a
    **new impact class** (audit-log disclosure/forgery/tampering) not claimed by the CVE.
  - Variant B: low-priv user granted OpenAPI permissions on the admin's auth record and
    enumerated the OpenAPI definitions — confirming the CVE's claimed
    `OpenApiPermissionController` surface that the repro stage had only described, not
    exercised.
  - Negative control: `POST /openapi/add` (admin-protected) correctly rejected the
    low-priv user.
- **Parity:** `full` — the broken-access-control primitive (authenticated low-priv user
  performing admin-only operations on a protected resource) is fully demonstrated on
  two additional controllers, and a new impact class (audit-log tampering) is shown.
- **Not demonstrated:** Chaining stolen credentials through `/openapi/call/**` to
  impersonate the admin end-to-end (same limitation as the parent repro; the
  `/openapi/call/**` SK-signature issue is a separate adjacent root cause and was not
  the focus of this variant).

## Root Cause

The Shiro security configuration
(`jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java`)
maps the catch-all `/**` to the `jwt` filter:

```java
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // no auth at all
...
filterMap.put("jwt", new JwtFilter(cloudServer==null));
filterChainDefinitionMap.put("/**", "jwt");               // auth-only, no authz
```

`JwtFilter` performs **authentication only** (it validates that a JWT exists and is
signed). Authorization is enforced **exclusively** via method-level annotations
(`@RequiresRoles` / `@RequiresPermissions`), which Shiro's
`AuthorizationAttributeSourceAdvisor` intercepts.

`OpenApiLogController` and `OpenApiPermissionController` declare **no** such annotations
on any method, so the authorization layer is never invoked for them. Any caller who
passes JWT authentication — including a user whose only role is `test` — can execute
every method. The sibling `OpenApiController` annotates its mutating endpoints with
`@RequiresRoles({"admin"})`, which is why its `POST /add` correctly rejects the low-priv
user (the negative control).

The same underlying bug (missing Shiro authorization annotations + auth-only `/**`
filter) is therefore reachable from multiple distinct controllers/endpoints — the parent
CVE exercised `OpenApiAuthController`; this variant exercises `OpenApiLogController`
and `OpenApiPermissionController`.

**Fix commit:** None exists. The parent RCA's recommended fix (add
`@RequiresRoles({"admin"})` to mutating endpoints in `OpenApiAuthController` and
`OpenApiPermissionController`) would still leave `OpenApiLogController` unprotected.

## Reproduction Steps

1. **Reference:** `bundle/vuln_variant/reproduction_steps.sh`
2. **What the script does:**
   - Resolves the JeecgBoot `v3.9.2` repo from the prepared project cache and records the
     exact tested commit (`git rev-parse HEAD` = `7df07a8…`).
   - Ensures MariaDB + Redis are running and the `jeecg-boot` database is seeded (reusing
     the seeded DB if present; importing the seed SQL otherwise).
   - Ensures the `jeecg-system-start-3.9.2.jar` is built (reuses the pre-built jar).
   - Starts the JeecgBoot Spring Boot application (or reuses an already-running instance)
     with login captcha disabled, then polls `/sys/randomImage/**` until it is healthy.
   - Logs in as the low-priv user `ceshi` (role `test`, password `123456`) and as `admin`.
   - **Variant A:** `ceshi` calls `GET /openapi/record/list` (reads all 27 audit logs),
     `POST /openapi/record/add` (creates a forged log attributed to the admin auth id),
     `DELETE /openapi/record/delete?id=<created>` (deletes it; verified in DB), and
     `GET /openapi/record/queryById`.
   - **Variant B:** `ceshi` calls `POST /openapi/permission/add` (grants permissions on
     the admin's auth id; 2 rows persisted) and `GET /openapi/permission/getOpenApi`.
   - **Negative control:** `ceshi` calls `POST /openapi/add` and confirms it is rejected
     with `Subject does not have role [admin]`.
   - Cleans up the rows it inserted so the run is idempotent.
   - Exit 0 = variant reproduced on the latest available code; Exit 1 = not reproduced.
3. **Expected evidence of reproduction:**
   - `http/record_list_ceshi.json` — `success:true`, `result.total=27`.
   - `http/record_add_ceshi.json` — `success:true` ("添加成功！"); persisted id verified in DB.
   - `http/record_delete_ceshi.json` — `success:true` ("删除成功!"); DB row count 1→0.
   - `http/record_queryById_ceshi.json` — `success:true`.
   - `http/permission_add_ceshi.json` — `success:true` ("保存成功"); 2 permission rows persisted against admin auth id.
   - `http/permission_getOpenApi_ceshi.json` — `success:true`.
   - `http/openapi_add_negcontrol.json` — `success:false`, `Subject does not have role [admin]`.
   - `logs/vuln_variant/reproduction_steps.log` — full execution log.
   - `logs/vuln_variant/jeecgboot_app.log` — application log.
   - `logs/vuln_variant/latest_version.txt` / `fixed_version.txt` — source-identity resolution.

## Evidence

- **Log file locations:**
  - `bundle/logs/vuln_variant/reproduction_steps.log`
  - `bundle/logs/vuln_variant/jeecgboot_app.log`
  - `bundle/logs/vuln_variant/latest_version.txt`
  - `bundle/logs/vuln_variant/fixed_version.txt`
  - `bundle/vuln_variant/http/*.json` — HTTP request/response captures for each proof.
- **Key excerpts proving the alternate trigger:**

  VARIANT A — low-priv user reads all audit logs (`http/record_list_ceshi.json`):
  ```json
  { "success": true, "code": 200,
    "result": { "records": [ { "id": "1922175238557913090",
      "apiId": "1922132683346649090", "callAuthId": "1922164194775056386", ... } ],
      "total": 27 } }
  ```

  VARIANT A — low-priv user creates then deletes an audit log:
  ```json
  // POST /openapi/record/add  (callAuthId = admin's auth id)
  { "success": true, "message": "添加成功！", "code": 200 }
  // DELETE /openapi/record/delete?id=2073800946032979970
  { "success": true, "message": "删除成功!", "code": 200 }
  // DB verification: row count for that id went 1 -> 0
  ```

  VARIANT B — low-priv user grants permissions on admin's auth record:
  ```json
  // POST /openapi/permission/add (apiAuthId = admin's auth id)
  { "success": true, "message": "保存成功", "code": 200 }
  // DB: 2 rows persisted in open_api_permission with api_auth_id=1922164194775056386
  ```

  NEGATIVE CONTROL — admin-protected endpoint correctly rejects low-priv user:
  ```json
  // POST /openapi/add  (has @RequiresRoles({"admin"}))
  { "success": false, "message": "Subject does not have role [admin]", "code": 500 }
  ```

- **Environment details:** MariaDB (jeecg-boot DB seeded), Redis, JeecgBoot v3.9.2
  Spring Boot 3 jar, JDK 17, low-priv user `ceshi`/role `test`, admin user `admin`/role `admin`.

## Recommendations / Next Steps

The (future) fix must cover **all** OpenAPI controllers that perform privileged
operations, not just `OpenApiAuthController`:

1. Add `@RequiresRoles({"admin"})` (or appropriate `@RequiresPermissions`) to **every**
   mutating endpoint in `OpenApiLogController` (`/openapi/record/add`, `/edit`,
   `/delete`, `/deleteBatch`) — this controller is **entirely absent** from the CVE
   description and the parent RCA's recommended fix.
2. Add `@RequiresRoles({"admin"})` to `OpenApiPermissionController.add()` and
   `getOpenApi()` (or restrict `getOpenApi` to the caller's own `apiAuthId`).
3. Reconsider the read endpoints `GET /openapi/list` and `GET /openapi/queryById` on
   `OpenApiController`, which also lack annotations and let any authenticated user
   enumerate all OpenAPI definitions (including `originUrl`/headers).
4. Defense-in-depth: do not rely solely on method annotations. Add an authorization
   rule in the Shiro filter chain for `/openapi/**` (e.g. require an admin role at the
   filter level for mutating paths), and enforce **ownership** checks (a caller may only
   mutate logs/permissions/credentials whose `systemUserId`/`apiAuthId` belongs to them).
5. Fix the adjacent `/openapi/call/**` issue: verify the SK signature, not just the
   `appkey` header, so stolen AK/SK pairs cannot be used without the secret.

## Additional Notes

- **Idempotency:** The script was run three times consecutively; all three completed
  with exit 0 and the variant reproduced each time. The script cleans up the rows it
  inserts (variant-test-api log row, variant-perm-api-* permission rows) so repeated
  runs do not accumulate state. It reuses an already-running JeecgBoot instance and an
  already-seeded database when present.
- **Source identity:** Tested commit `7df07a823fd558be857d0208ccae96342539fbc1`
  (tag `v3.9.2`). `master` HEAD `32a8dfe94f014f7a9d472bce83e08bb8cf9c10c3` was
  byte-diffed and is identical for all four OpenAPI controllers — i.e. the variant is
  confirmed on the latest available code, not just an old tag.
- **Why this is a variant, not the same trigger relabeled:** Variant A targets a
  **different controller** (`OpenApiLogController`), a **different URL prefix**
  (`/openapi/record/*`), a **different protected resource** (the audit-log table), and
  produces a **different impact class** (audit-log tampering/destruction) — none of which
  appear in the CVE description. Variant B targets `OpenApiPermissionController`, which
  the CVE description mentions but the parent repro never exercised at runtime.
- **No SECURITY.md / threat model** exists in the JeecgBoot repository that scopes these
  endpoints as out-of-scope or "by design"; these are network-reachable authenticated
  admin-management endpoints, so the broken-access-control finding is within scope.
