Skip to content

JeecgBoot coordinated wave (76 GHSAs): annotation-absence as the authz model, self-approval workflows, and an AI module full of credential oracles

Wave in one line: Seventy-six advisories against JeecgBoot ≤3.9.5 (Java low-code platform, Spring Boot + Apache Shiro) published in one batch at 2026-10-11T00:30Z — almost every one is a handler that simply lacks the @RequiresPermissions annotation its siblings carry, which makes annotation-absence the authorization boundary for the whole product class: any authenticated session reaches every un-annotated route, including DB-credential, MCP-token, tenant-ownership, and audit-log-wipe endpoints.

Signal: 76 GHSAs (7 high, 69 medium, CVE-2026-108605–108680), all through 3.9.5. Representative legs below; the full set is the sweep lesson itself. JeecgBoot ships a large /sys/** + /airag/** + /openapi/** management surface out of the box and is common in the wild as internally-built business apps — a high-yield bug-bounty and engagement target.

Authorized validation only

Disposable instance, throwaway low-privileged user, synthetic rows/tenants, recorder/rollback for every mutation. Never enumerate a third-party instance, read real DB credentials, or delete audit tables outside an approved lab.

High-severity legs

GHSA CVE Boundary
xqq5-rxcj-4fmx CVE-2026-108648 GET /sys/api/getDynamicDbSourceByCode has no Shiro permission/role annotation at all — any authenticated low-priv user supplies dbSourceCode and gets JDBC URLs, usernames, and decrypted cleartext database passwords for every configured dynamic datasource. SystemApiController is an internal aggregation API (role/department/user/query helpers) that assumed only "trusted" callers would ever address it directly.
ghh4-x4m2-g9rc CVE-2026-108677 GET /sys/api/getUserByName returns any user's stored password value, AES-CBC wrapped — with the key handed to you by /sys/getEncryptedString, an unguarded endpoint that exists to encrypt login payloads. Decrypt admin password ciphertexts offline. Response "encryption" with an app-served key is obfuscation, not protection.
p9cq-jf42-732x CVE-2026-108671 AI module reads MCP server configurations — endpoint URLs, headers, and outbound auth tokens — because the queryById permission check is commented out; record ids come from the unguarded /airag/app/queryById. A commented-out authz check that stays in the shipped source is a standing door with a label on it.
hh6j-j8mj-f737 CVE-2026-108661 POST /sys/tenant/changeOwenUserTenant [sic] transfers tenant ownership given any userId+tenantId — no check that the caller is the current owner. A low-priv member reassigns any tenant's owner to themselves and strips the real owner. (The route-name typo OwenUser is itself a fingerprint — grep bundles for misspelled handlers; they correlate with skipped review.)
gp9m-m3c8-jw96 CVE-2026-108657 Self-approval workflow: file a tenant-admin application via doApplyTenantPackUser, approve it yourself via PUT /sys/tenant/passApply — requester and approver are the same principal, any tenant. Separation-of-duties is a route-pair property: test can(requester) == can(approve).
5xxx-327x-j7r9 CVE-2026-108628 saveDeptRolePermission — any authenticated user writes department role permissions (roleId + permissionId bindings), the direct self-grant primitive for the RBAC engine itself.
6vff-68pc-g8vm CVE-2026-108623 SysLogController deleteBatch accepts ids=allclear, a magic sentinel that wipes the entire sys_log audit table for any authenticated caller. Bulk-mutation verbs with sentinel values are both an integrity and an anti-forensics surface.

Notable medium legs (the class, not the count)

Pattern Legs Detail
Unguarded list = ID oracle for the gated verb ~20 of 76 The recurring sentence "attackers can obtain ids from the unguarded /list endpoint" — list/queryById siblings of gated create/update/delete expose every row id (logs r6fq, prompts pcc8, OCR wf7x, OSS files 8fjj, announcements, positions, depart-permissions…). Gate parity must be checked pairwise: id-source route vs consuming route.
AI knowledge/prompt oracles fw8c/108669, pvqc/108673, 47h3/108670 embeddingSearch reads document text chunks from any knowledge base (no annotation); exportXls bulk-downloads every user's AI prompts (model ids, params) as one workbook; promptExperiment burns LLM quota running other users' prompt templates against their datasets.
Shared-state prompt poisoning 392w/108605, wf7x/108606 Global OCR templates live in a single shared Redis key (airag:ocr) — any user's PUT /airag/ocr/edit overwrites the LLM prompt every other user's OCR calls consume. Shared cache namespace == cross-user injection channel.
AI record IDOR by userId param x5p6/108672, 6rqr/108609, c23h/108608, 6wfc/108607 Video/voice generation history read and deleted by supplying another user's userId — the session identifies the caller, the query identifies nobody.
Import/export verb-family bypass jqgc/108647, xrqv/108646, ww57/108636 importExcel handlers lack the add permission their CRUD twin enforces — upload a workbook to bulk-create validation rules (arbitrary regex!), categories, departments. Verb coverage on /import, /export, /batch, /revert, /recycleBin is routinely missed by annotation passes.
Trusted-internal relay abuse cmc8/108680, cm6g/108679 sendTemplateAnnouncement/sendBusAnnouncement accept forged sender/recipient/title — any user delivers messages rendered as admin/system-origin (phishing relay with a trusted sender badge).
Tenant enumeration by small integers rghv/108658, x5p8/108659, xp5m/108656 queryTenantAuthInfo, listPackByTenantUserId, getTenantPackApplyUsers iterate tenantId=1..N to map other tenants' records and admin applications. Multi-tenant IDs are guessable; every tenant-scoped route deserves an increment probe.

Why this is durable operator value

  1. Annotation-absence means annotation-presence is the entire allow-list — enumerate handlers, not roles. Shiro's @RequiresPermissions is opt-in per method: a missing annotation is not "deny by default," it is reachable by every session. The sweep for any Spring/Shiro app: list controller methods (actuator mappings, swagger export, or source), diff annotated vs un-annotated, and treat every un-annotated one as an authenticated-user endpoint until proven otherwise. JeecgBoot's SystemApiController shows the sharpest shape of this: an internal aggregation API (/sys/api/*) built for trusted server-side callers, exposed on the same public port with the same session filter — the "API for the frontend/internal relay" family deserves its own route dump on every Java app you test.
  2. Pairwise gate parity: every id-consuming verb has an id-source route; gate both. Twenty-plus legs here follow one template: DELETE /x/delete is annotated, GET /x/list is not, so the gate protects nothing. Audit rule: for every mutation route, find where its ids come from (list, queryById, export) and require the same minimum privilege on both. This is the Oct 10 HaiND sibling-gate-diff rule (Octop /api/providers vs /api/admin/providers) generalized to id-supply routes.
  3. Workflow routes are authorization state machines — test requester==approver. The doApplyTenantPackUser → passApply self-approval and the owner-unchecked changeOwenUserTenant transfer are the two generic probes for any approval/transfer/invite feature: (a) can the principal who creates a request also satisfy it? (b) does the transfer endpoint verify the caller holds the thing being transferred? Also grep for the misspelled-handler class: typo'd route names (changeOwenUserTenant, Owen for Owner) mark code paths that skipped naming review — fuzz bundles for common misspellings.
  4. App-served crypto keys defeat app-served encryption. /sys/getEncryptedString exists to make logins look encrypted; the same key decrypts password ciphertexts returned by an unguarded route. For any app with a key/init/bootstrap endpoint (login encrypt keys, RSA pairs, signature secrets), assume every other use of that key is confidentiality-zero. Probe: capture the key endpoint, try it against every "protected" response field you can obtain.
  5. Sentinel parameters on bulk verbs. ids=allclear is a sentinel the handler interprets as "everything." Replay-test bulk delete/update endpoints with empty value, *, all, allclear, and framework-flavored sentinels against lab rows — and note audit-log deletion specifically: it changes the cost of noisy testing on any platform you operate against with scope.
  6. The AI sidecar module is the least-reviewed attack surface. AI/LLM features bolted onto mature CRUD platforms reuse the platform's (broken) authz and add new shared-state classes: per-feature shared Redis keys as global prompt stores, MCP/datasource config tables as outbound-token vaults, embedding search as cross-tenant corpus read, prompt export as bulk IP/credential harvest, and generation history keyed by a caller-supplied userId. When a platform gains an airag-style module, diff its controllers against the core app's annotation coverage first — new module, old bug.
  7. Commented-out checks are live intelligence. The MCP-config leg's check is // permission in the shipped source. For open-source Java apps, git log -S 'RequiresPermissions' (comment-out diffs) and grep for commented authz lines finds siblings the advisory wave didn't name. Same-product re-sweep applies: one missing-annotation wave predicts the next.

Replayable validation boundaries (lab/authorized scope only)

  • Annotation-absence sweep (own JeecgBoot-class lab): dump route inventory (/v2/api-docs swagger export or actuator mappings) with a throwaway low-priv session; request every /sys/api/*, /airag/*, /openapi/* route; table status codes + payload presence. Evidence = route→privilege matrix. Never run this against a third-party host without written scope.
  • Id-source parity probe (lab rows): for one resource family, confirm list returns ids the gated mutation consumes, with the lowest-tier token. Marker rows only; record field names, not values.
  • Self-approval decision table (two disposable tenants): apply → approve as same principal → assert privilege delta (tenant-admin pack granted). Revert after capture.
  • Key-endpoint decryption check (lab user set): fetch /sys/getEncryptedString, request your own user's record via the unguarded read, decrypt locally to prove the wrap. Never target another real user's material outside the lab.
  • Sentinel bulk-verb test (scratch rows only): bulk-delete one scratch row, then ids=allclear against a seeded scratch table in an isolated schema, with audit export first. Never point this at sys_log of a shared instance.
  • Shared-key prompt poisoning (lab Redis): snapshot the airag:ocr-style key, write a marker template via the user route, observe another lab user's call consuming it, restore. Never touch production caches.

October 11 (03:30Z) same-template legs from the following wave

The Oct 11 03:30Z batch repeated this page's templates verbatim on other products — evidence the annotation-absence/id-source-parity sweep generalizes beyond JeecgBoot: eladmin GET /api/s3Storage/download/{id} sequential-id enumeration returns every stored object URL without storage permission (GHSA-8j2c-pwgm-76r5) and POST /api/localStorage/pictures image-named upload bypasses storage:add + leaks absolute paths (GHSA-2hch-3q25-w3g9); MultiVendorX marketplace settings REST route gated only by edit_stores lets any store_owner rewrite commission/payout settings site-wide — verb-scope drift on a settings namespace (GHSA-h37j-r28p-597w); Wukong AICRM POST /chat/send accepts an arbitrary sessionId, letting any user append into and stream replies from a victim's AI conversation (AI-module id-source parity again, GHSA-2p74-f492-56xr). Wukong_HRM/CordysCRM aspect/annotation waves folded on the Sept 21 flat-authz drift page.

October 11 (15:3xZ) second wave: ~24 more GHSAs — config-writes, row-rule tampering, and forged system announcements

Fifteen hours after the 76-GHSA dump, a second JeecgBoot ≤3.9.5 batch (CVE-2026-108866–108891) landed with the same annotation-absence template plus a few new verb families — same-product re-sweep in action; expect a third. Legs with new axes:

GHSA CVE Boundary
cc33-2w5g-wpvx CVE-2026-108883 (high) editThirdAppConfig — any authenticated user rewrites the DingTalk/WeCom/Feishu integration's client id, client secret, agent id, and corp id. That is a standing redirect of directory sync + all platform messaging to attacker-controlled apps (credential swap → message-path takeover), not just a config write. Config-write routes that re-point integrations are message-relay and identity-sync primitives — test them before theorizing about the messaging routes themselves.
898h-4prp-w6gp / w6fw-28rx-2577 CVE-2026-108870 / 108871 saveDatarule on both the role and department-role controllers lets any user overwrite data_rule_ids — direct row-level filter tampering: clear a rule to widen readable rows, or set one to steer which rows any role sees. RBAC engines with a data-rule layer put the row-scope policy behind the same annotation gate as the feature itself; probe the rule-write route even when the feature route is gated.
4qg9-8c9f-qcv4 / vxv9-jpp4-r7f7 CVE-2026-108869 / 108868 POST /sys/api/sendSysAnnouncement and sendBusTemplateAnnouncement accept forged fromUser/toUser/title/content — any user delivers announcements rendered as system-origin over WebSocket/WeCom. The /sys/api/* internal-relay family from the first wave gains the message-spoofing verb set; phishing relay with a trusted sender badge, same template as the first wave's cmc8/cm6g.
623c-j748-v6w2 CVE-2026-108881 getTenantPackInfo with fixed packCode values (superAdmin, accountAdmin, appAdmin) discloses which tenants hold which admin packs — a tenant→role map from a single guessed-code read. Enumerate well-known pack/plan/role code constants on any multi-tenant platform; the code list often ships in the frontend bundle.
2274-mgcg-m5j6 CVE-2026-108880 PUT /sys/dict/editDictByLowAppId — dictionary edit gated (if at all) by a client-supplied lowAppId parameter or X-Low-App-ID header. Where the "scope" selector is attacker-chosen, the gate checks the selector, not ownership — same supplied-scope-vs-owned-scope rule as the Oct 8 DolphinScheduler legs.
5w4w-m38r-7r7r CVE-2026-108879 /airag/api/getChatVariable reads any user's AI chat variables by supplying a username param — the AI-module caller-supplied-identity IDOR family extends to chat state.
7xj8-xwgj-5prc / h752-2r87-c7w9 CVE-2026-108866 / 108867 queryUserAuths and getUserRoleSetById return any user's full permission set / role codes by userId — admin-account discovery without touching a user-list route. Privilege-map oracles make every later targeted probe cheaper; sweep *Auth*/*Role* read routes independently of user enumeration.
pmhm-r7h7-cxvj CVE-2026-108876 putCancelQuit restores ended/refused tenant memberships with a caller-supplied tenantId or X-Tenant-Id header — tenant-header re-selection on a state-change verb.

Updated rules: (1) the annotation-absence sweep is exhaustive, not one-shot — two waves fifteen hours apart against the same band means the first dump is a floor, not a ceiling; re-poll the vendor and re-run the handler diff before closing a JeecgBoot-class engagement. (2) Integration config-write routes (editThirdAppConfig-style) are higher-value than they score: swapping IdP/messaging app credentials converts a low-priv write into a standing directory-sync + message-path interception position. (3) Row-level data-rule stores are authorization data, behind feature-gated writes — audit rule-write routes separately from feature gates.