The Annotation Was Commented Out: 76 JeecgBoot CVEs, 22 in the AI Module, and a Fix That Only Exists in Main

At 22:16 UTC on 10 October 2026, NVD published 76 CVEs against JeecgBoot in a single minute. They are contiguous — CVE-2026-108605 through CVE-2026-108680, with no gaps — every one assigned by VulnCheck, every one carrying the same affected statement: JeecgBoot through 3.9.5. Seventy are CWE-862 (missing authorization); the remaining six are CWE-639 (authorization bypass through user-controlled key). None of the 76 NVD records carries a CPE configuration, and none names a fixed version.

JeecgBoot is a Java low-code development platform with 48,157 stars and 16,215 forks on GitHub. Version 3.9.5 — the version the entire batch is scoped to — is the newest release, published 27 August 2026. There is no 3.9.6.

Twenty-two of the 76 land in jeecg-boot-module-airag, the platform’s AI module: prompt templates, knowledge-base embeddings, OCR templates, AI video and voice records, evaluator datasets, and MCP server configurations. That is the part of this batch worth reading closely, and it is where we found something the aggregators have not reported: for eight of these CVEs the fix already exists in the repository, written a month before disclosure, and it has never been released.

The commented-out annotation

Start with the highest-severity AI finding, CVE-2026-108671 (CVSS 4.0 base 7.1, AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N). The NVD description says any authenticated user can read MCP server configurations “because the queryById permission check is commented out,” retrieving MCP endpoint URLs, headers, and outbound authentication tokens.

We read AiragMcpController.java first-hand at commit e3b9dc0a — the commit tagged v3.9.5版本发布, dated 26 August 2026, which is what 3.9.5 ships. The description is literal:

    @Operation(summary = "MCP-通过id删除")
    @RequiresPermissions("airag:mcp:delete")
    @DeleteMapping(value = "/delete")
    ...

    @Operation(summary = "MCP-通过id查询")
    //@RequiresPermissions("airag:mcp:queryById")
    @GetMapping(value = "/queryById")
    public Result<AiragMcp> queryById(@RequestParam(name = "id", required = true) String id) {

The handler immediately above it keeps airag:mcp:delete. The handler immediately below keeps airag:mcp:export. Only the read is disabled, and it is disabled by two slashes — the signature of a check switched off during development and never switched back on. The object it returns is an AiragMcp row: the connection details an agent uses to reach an MCP server, including whatever token sits in its headers.

This is the distinguishing property of the airag cluster. In the rest of the batch, a missing @RequiresPermissions exposes a department record or a message template. Here it exposes outbound credentials for a machine-to-machine tool protocol, held as ordinary CRUD rows in a low-code admin console and governed by the same annotation discipline as the announcements table.

The fix was written on 11 September and never shipped

We checked whether the annotation is still commented out on the default branch. It is not. At main HEAD — commit 87d7f938d47d, 22 September 2026 — the line reads @RequiresPermissions("airag:mcp:queryById"), uncommented.

The change came from commit 21facc013906, 11 September 2026 at 09:53 UTC, with the commit message AI提示词管理加权限注解 — “add permission annotations to AI prompt management.” It touches three files. It uncomments the MCP line. It adds twelve @RequiresPermissions annotations to AiragPromptsController, covering list, recycleBinList, add, edit, delete, deleteBatch, queryById, revertRecycleBin, deleteRecycleBin, experiment, exportXls, and importExcel. And it adds a Flyway migration to create the matching button permissions, named V3.9.5_1__add_airag_prompts_button_permissions.sql.

Match that against the CVE list and the overlap is exact. The commit remediates CVE-2026-108664 (prompts queryById), 108665 (edit), 108666 (deleteBatch), 108667 (revertRecycleBin), 108668 (deleteRecycleBin), 108670 (promptExperiment), 108673 (exportXls), and 108671 (MCP queryById) — eight of the 76, and it covers several endpoints that were never assigned a CVE at all.

The maintainers found this class of defect themselves, in the AI module specifically, sixteen days after cutting 3.9.5 and a month before VulnCheck published. They then did not cut a release. The migration file is even named for 3.9.5, as a patch-level addition to the current version. The last push to main was 22 September; as of our checks on 11 October there is no 3.9.6, and the newest non-draft release remains v3.9.5 from 27 August. Every installable copy of JeecgBoot still has the annotation commented out.

We have written this shape up repeatedly in the last ten days — PraisonAI’s fixed version that was never published to PyPI, CISA’s openPDC advisory pointing at nightly builds with no release object, nginx-ui’s ten advisories resolved only to Go pseudo-versions. JeecgBoot is the cleanest instance yet, because here the gap is not a metadata error in the advisory. The advisory is correct: there is no fixed version. The fix is simply sitting on a branch.

The other fourteen AI-module findings, and what they reach

The airag CVEs that commit 21facc01 does not touch are unfixed everywhere, including on main:

  • CVE-2026-108669 — knowledge-base embedding search. AiragKnowledgeController.embeddingSearch carries no Shiro annotation; supplying knowledge-base ids to GET /airag/knowledge/embedding/search returns document text chunks from knowledge bases the caller has no grant for. This is a RAG corpus read across the tenant boundary, through the retrieval path rather than the file path.
  • CVE-2026-108605 and 108606 — OCR templates. /airag/ocr/edit lets any authenticated user overwrite LLM prompts in the shared airag:ocr Redis template set; deleteById removes records enumerated from the unguarded /airag/ocr/list. The first is prompt modification against a shared, system-wide template — an injection primitive that persists in configuration rather than in a document.
  • CVE-2026-108613 — AI application release. POST /airag/app/release publishes or unpublishes another user’s AI application and, per the record, yields share tokens.
  • CVE-2026-108614, 108615, 108616 — evaluator data. Export, delete and batch-delete on /airag/extData/*: every user’s evaluator definitions downloadable as XLS, any record removable.
  • CVE-2026-108607, 108609, 108672, 108608 — generated media records. Four CWE-639 entries where a caller-supplied userId selects whose AI video or voice history is read or deleted. /airag/voice/listByUser returns submitted text-to-speech input for an arbitrary user id.
  • CVE-2026-108610, 108611, 108612 — Word templates. Edit, delete and batch-delete against the shared /airag/word/* template set.

Read together, the AI module reproduces the host platform’s authorization model exactly — including its omissions. Nothing here is an AI-specific vulnerability class. There is no prompt injection, no model extraction, no tool-call confusion. It is CWE-862, the oldest kind of web bug, applied to objects that now hold prompts, retrieval corpora, and MCP credentials. The AI feature set inherited the console’s access-control debt wholesale, and the newest code in the repository is the code with the least annotation coverage.

Encryption used as an authorization control

Two findings outside the airag module are worth isolating, because together they show the same substitution — a confidentiality mechanism standing in for an access check — and because we could verify the whole chain in public source.

CVE-2026-108677 (7.1) covers GET /sys/api/getUserByName in SystemApiController. At v3.9.5 the handler has no permission or role annotation at all. It fetches a LoginUser and then calls SensitiveInfoUtil.handlerObject(loginUser, true), commented 用户信息加密 — “encrypt user info.” So the stored password value is returned, AES-CBC encrypted rather than withheld.

The CVE record says the response can be decrypted “using the hard-coded key exposed by /sys/getEncryptedString.” Both halves check out. EncryptedString.java in jeecg-boot-base-core declares public static String key = "1234567890adbcde" and iv = "1234567890hjlkew" — the defaults are in the public repository, unchanged at main HEAD. And LoginController.getEncryptedString() returns both values as a JSON map, while ShiroConfig line 113 registers filterChainDefinitionMap.put("/sys/getEncryptedString", "anon"). The key and IV protecting the password field are served to unauthenticated callers by design, because the browser needs them to encrypt the login form.

CVE-2026-108648 (7.1) is the same pattern without even the encryption. GET /sys/api/getDynamicDbSourceByCode has a bare @GetMapping and returns a DynamicDataSourceModel — JDBC URL, username, and, per the advisory, decrypted cleartext database password — for any datasource code an authenticated low-privileged user cares to guess. We confirmed the handler is unannotated at v3.9.5 and at main HEAD. So is saveDeptRolePermission (CVE-2026-108628, 8.6, the joint-highest in the batch), which grants arbitrary menu and button permissions to a role id of the attacker’s choosing. The September cleanup reached the AI module and stopped there.

Why the severity numbers understate this

The score distribution is 69 at 5.3, five at 7.1, two at 8.6. Scanned individually, almost the whole batch reads as medium-severity housekeeping, and each record is individually accurate: one endpoint, one missing annotation, bounded impact, PR:L because you need an account.

The aggregate is a different object. Any authenticated low-privileged account can, in sequence: read every tenant’s records and transfer tenant ownership to itself (CVE-2026-108661, 7.1) or approve its own tenant-administrator application (CVE-2026-108657, 8.6); grant itself arbitrary menu permissions (108628); extract cleartext datasource credentials (108648) and administrator password ciphertext decryptable with a publicly documented key (108677); and wipe the entire sys_log audit table by sending ids=allclear to the batch-delete endpoint (CVE-2026-108623, 7.1). The last one matters disproportionately: on a platform where 70 findings are “a low-privileged user did something they should not have,” the audit log is the only control that would show it, and it is deletable by the same account.

CVSS has no vector for “and 75 others.” That is an argument for reading batch disclosures as a single posture statement rather than as 76 queue items — the same argument that applies to VulnCheck’s seven-CVE agent batch published the same day, where no individual score exceeded 6.0 either.

Public proof-of-concept, published before NVD

Every one of the 76 records links three references: a VulnCheck advisory, a permalinked GitHub source range in jeecgboot/JeecgBoot at commit e3b9dc0a, and a Python proof-of-concept in the public repository AnkesKasty/cve-request-poc. That repository was created 10 October 2026 at 14:31 UTC and last pushed at 14:47 — roughly seven and a half hours before the NVD records appeared — and its JeecgBoot directory contains 138 files. Scripts are shared across related findings; poc_airag_data_plane.py, for instance, is referenced by both the knowledge-base embedding search and the prompt-experiment CVEs.

So the disclosure arrives with working exploitation scripts already public, pinned source lines for every finding, and no version to upgrade to. That ordering is worth stating plainly rather than editorialising about: defenders evaluating this batch have precise detection material and no patch.

What to do

  • Do not wait for a version bump. There is nothing to upgrade to. If you run JeecgBoot 3.9.5 or earlier and can build from source, commit 21facc013906 plus its Flyway migration is the upstream-authored fix for the prompt and MCP findings — but it is eight of 76, and it leaves the tenant, datasource, permission and audit-log findings untouched.
  • Treat low-privileged platform accounts as console-administrative. The realistic reading of this batch is that PR:L and PR:H are not meaningfully separated in JeecgBoot 3.9.5. Scope account issuance, SSO group mappings and self-registration accordingly, and do not rely on the role model to contain a compromised ordinary user.
  • Rotate anything reachable through /sys/api/getDynamicDbSourceByCode and /airag/airagMcp/queryById. Datasource credentials and MCP outbound tokens held in the platform should be considered readable by every authenticated account. MCP tokens in particular are credentials to other systems; rotating them is independent of patching JeecgBoot.
  • Override the default AES key and IV. EncryptedString.key and .iv ship as 1234567890adbcde and 1234567890hjlkew and are served unauthenticated by /sys/getEncryptedString. Changing them does not fix CVE-2026-108677 — the endpoint still hands out whatever the current values are — but a non-default key removes the offline, source-only path for anyone who merely scraped a response earlier.
  • Ship audit logs off-box. sys_log is wipeable by an authenticated low-privileged user via ids=allclear. Forward to external storage the platform cannot reach.
  • Audit your own AI modules for annotation coverage, not for AI-specific bugs. The lesson in the airag cluster generalises: when prompt libraries, RAG corpora and MCP connection records become rows in an existing admin CRUD framework, they inherit that framework’s authorization discipline. A linter that fails the build on an unannotated controller method would have caught every one of these 76 findings, including the one that was commented out.

Verification note: the batch composition — 76 contiguous CVE identifiers, publication timestamps, CWE assignments, CVSS 4.0 vectors and base scores, assigning source (disclosure@vulncheck.com for all 76), absence of CPE configurations, and every description quoted or paraphrased above — was read first-hand from the NVD 2.0 API on 11 October 2026; all 76 records were at status Received with no independent NVD analysis. Source excerpts, annotation presence and absence, the commented-out MCP line, EncryptedString default key and IV values, the ShiroConfig anon registration, and the getUserByName and getDynamicDbSourceByCode handlers were read first-hand from raw files in jeecgboot/JeecgBoot at commit e3b9dc0a (v3.9.5) and at main HEAD 87d7f938d47d. Commit 21facc013906, its date, message, three changed files and annotation diff were read first-hand from the GitHub commits API; release tags, dates, draft status, star and fork counts and last-push date likewise. The proof-of-concept repository creation time, visibility and file count were read first-hand from the GitHub API. The mapping of commit 21facc01 to eight specific CVEs, the observation that the fix exists only on main, the reading of encryption-as-authorization, and the argument that the aggregate exceeds the individual scores are our editorial analysis and are not claims made by VulnCheck, NVD or the vendor. We ran no proof-of-concept, sent no requests to any JeecgBoot instance, and tested nothing.

Sources: