Twenty AI Platforms in Eighteen Seconds: VulnCheck’s 122-CVE Batch and the Decorator That Only Looked Like Auth

Between 18:17:38.950Z and 18:17:56.980Z on 16 October 2026 — eighteen seconds — VulnCheck pushed 122 CVE records into NVD. We counted them directly from the NVD 2.0 API by assigner: every one carries disclosure@vulncheck.com, every one is vulnStatus: Deferred, and the publication timestamps run in an unbroken sub-second cadence from the first record to the last. Most of the batch is the ordinary long tail of self-hosted web software: ERP suites, CMSes, payment gateways, a router’s Boa web server. But twenty of the 122 records, spanning sixteen distinct products, are AI platforms — model gateways, chat front-ends, RAG knowledge bases, MLOps consoles, AI observability stacks — and read side by side they are not twenty bugs. They are one bug, found twenty times.

The first record in the batch, at 18:17:38.950Z, is CVE-2026-111392 in SGLang, which we covered on its own yesterday because its mechanism — a caller naming the prefix-cache key for an image — is genuinely novel. The other nineteen are the opposite of novel, and that is the story. This is the second mass VulnCheck batch we have taken apart this month, after eighteen filename command-injection CVEs in nineteen seconds on 14 October, and the third credential-cluster of the month after one researcher’s 11 October sweep through seven agent gateways. Every advisory in the AI subset credits the same handle, hieuPenguinnn, with a public source-review writeup alongside it.

The twenty records

All figures below are CVSS 4.0 base scores and CWEs as recorded by NVD at the time of writing.

  • 9.3 — CVE-2026-111692 and CVE-2026-111693, DataBuff ai-apm-web ≤ 0.1.10 (CWE-321 hard-coded signing secret; CWE-1392 default admin credentials)
  • 9.3 — CVE-2026-111704, LangChat (CWE-306, no authentication on any REST controller)
  • 9.2 — CVE-2026-111636, OpenBMB StaffDeck ≤ 0.5.6 (CWE-321, forgeable admin session tokens)
  • 8.7 — CVE-2026-111681, EFAK ≤ 5.1.0 (CWE-862, self-promotion to ROLE_ADMIN); CVE-2026-111702, ChatOllama (CWE-639, private knowledge bases by sequential id); CVE-2026-111706, 99AI ≤ 4.3.0 (CWE-306, unauthenticated bulk chat-log export)
  • 7.1–7.2 — CVE-2026-111682, EFAK (unmasked LLM provider keys via /api/model-config); CVE-2026-111671, Cube-studio (stop/reset/redeploy other tenants’ notebooks and inference services); CVE-2026-111680, APIPark (cross-team service and app takeover via a team query parameter)
  • 6.3–6.9 — CVE-2026-111623, Xiaozhi-esp32-server-java (device WebSocket tokens issued to any MAC address, exposing private role system prompts and LLM/STT/TTS credit); CVE-2026-111617, WHartTest (unauthenticated UI-automation WebSockets); CVE-2026-111618, CoAI / Chat Nio (unauthenticated archive path traversal); CVE-2026-111653, 53AIHub (unauthenticated enterprise-profile overwrite); CVE-2026-111707, 99AI (cross-user single-message read)
  • 5.3 — CVE-2026-111630, abp-next-admin AIManagement (read and inject into another user’s AI conversation by GUID); CVE-2026-111656, ChatWiki (cross-tenant form filters); CVE-2026-111668, AIHelms (organisation-wide AI spend without the permission check); CVE-2026-111672, Cube-studio (pipeline DAGs, cluster names, namespaces by id); CVE-2026-111715, Arkon (cross-department wiki graph)

Seven of the twenty score 8.0 or higher. Thirteen of the twenty are CWE-306, CWE-862, CWE-639 or CWE-863 — four different identifiers for the sentence nobody checked whether this caller was allowed. The remaining shapes (CWE-321, CWE-1392, CWE-290) are the same omission moved one layer down: the check exists, but the secret that makes it meaningful ships in the repository.

EFAK: the allowlist names the pages, the REST API does the work

We read EFAK’s WebSecurityConfig.java at the v5.1.0 tag ourselves. There is exactly one SecurityFilterChain, and its authorisation rules are four lines long:

.requestMatchers("/login", "/error/**", "/statics/**", "/css/**", "/js/**",
                "/images/**", "/fonts/**", "/webfonts/**", "/sm/**",
                "/plugins/**", "/health/**")
.permitAll()
.requestMatchers("/users", "/config", "/scheduler", "/password-tool",
                "/api/password/**")
.hasRole("ADMIN")
.anyRequest().authenticated())

/users is the server-rendered account administration page. /config is the model configuration page. Both correctly require ROLE_ADMIN. The REST backends that perform the writes live at /api/users and /api/model-config, and only /api/password/** was ever added to the admin list — so both fall through to anyRequest().authenticated(), which any ordinary account satisfies. CVE-2026-111681 is the consequence on the write side: the create and update handlers copy roles out of the request body, so a ROLE_USER account grants itself ROLE_ADMIN and inherits control of the managed Kafka clusters. CVE-2026-111682 is the consequence on the read side: /api/model-config/page returns stored LLM provider API keys unmasked to the same ordinary account. We checked the default branch at the time of writing: the matcher list is byte-identical to v5.1.0. Nothing has moved.

This is worth naming precisely, because it is not an absent security layer. EFAK enables method security globally and PasswordController uses it. The gate exists, it is configured, it is applied — to the URL space a human clicks. The URL space a program calls was simply a different list, and the two lists were maintained by hand.

99AI: the decorator that looks like authentication and is documentation

The sharpest artefact in the batch is seven lines of TypeScript, which we read at the v4.3.0 tag:

@Post('exportExcel')
@ApiOperation({ summary: '导出问答记录' })
@ApiBearerAuth()
exportExcel(@Body() body: ExportExcelChatlogDto, @Res() res: Response) {
  return this.chatLogService.exportExcel(body, res);
}

Every sibling route in the same controller carries @UseGuards(JwtAuthGuard) or @UseGuards(AdminAuthGuard). This one does not. What it carries instead is @ApiBearerAuth(), which is a Swagger annotation: it tells the generated API documentation to display a padlock. It enforces nothing. On a screen full of decorators, the route that is missing its guard is the route that most looks like it has one — and because 99AI registers no global APP_GUARD, a route with no guard is anonymous by default rather than denied by default. CVE-2026-111706 (8.7) is an unauthenticated caller paging an XLSX of every user’s username, email, prompt and model answer, optionally filtered to one victim’s email address. We checked the project’s default branch: the handler is still guardless there today.

Two design decisions compound into that outcome, and neither is exotic. Fail-open routing means an omission is silent. A documentation decorator that borrows the vocabulary of authentication means the omission is also invisible to review. Neither is a bug on its own; together they make “forgot one line” indistinguishable from “wrote the line” at a glance.

LangChat, DataBuff, StaffDeck: the credentials were always going to be the prize

LangChat (1,313 stars) draws the batch’s only other 9.3 for a missing-authentication finding, and the reason is structural: no Sa-Token interceptor is registered, so the REST controllers are simply open. We read AigcUserController at the affected commit — a plain @RestController mapped to /api/v1/auth/users with list, detail, create, update and delete handlers and not one authorisation annotation between them. We then read the login path in the same module, where the credential comparison is:

.filter(candidate -> candidate.password() != null && candidate.password().equals(request.password()))

That is a plaintext string comparison, which is why the advisory can say an unauthenticated caller reads passwords rather than hashes. Note the dates honestly: the affected commit 2a6285a is the repository’s HEAD, last pushed 27 September 2026, so the published state of the project is the vulnerable state.

DataBuff’s two 9.3s need no analysis at all, only a quotation from application.yml at v0.1.10:

  security:
    seed-username: admin
    seed-password: Databuff@123
    jwt:
      secret: dev-secret

CVE-2026-111693 is logging in as admin. CVE-2026-111692 is skipping that step and signing your own token with dev-secret. StaffDeck (OpenBMB, 1,997 stars) is the same shape with better manners: its session signer is textbook — hmac.new(secret, body, sha256) verified through hmac.compare_digest, which is the constant-time comparison you want — and the secret it uses is the Pydantic settings default app_secret: str = "change-me-in-development", still the default on the project’s main branch at the time of writing. The cryptography is correct and the key is public. A default value that spells out its own remediation in the string literal is not a warning; it is a published key, because the deployments that most need the warning are precisely the ones that never read it.

What the AI layer adds to an old bug class

Missing authorisation in self-hosted web software is the oldest finding in the catalogue, and a reader is entitled to ask what is new. The answer is in what sits behind the unguarded handler. Across these twenty records the assets reached are: LLM provider API keys in cleartext (EFAK, LangChat, and by tenant-admin takeover StaffDeck), complete conversation transcripts with the usernames and emails attached (99AI twice, abp-next-admin, Xiaozhi), private RAG knowledge bases and their retrieved document text (ChatOllama), role system prompts (Xiaozhi), production inference endpoints and notebooks that can be stopped or redeployed (Cube-studio), and organisation-wide AI spend data (AIHelms). An IDOR on an invoice leaks an invoice. An IDOR on a chat platform leaks whatever the organisation has been telling a model all quarter, plus the key to keep asking it on someone else’s account.

There is a second-order effect that the CVSS vectors do not encode. A stolen provider key is not only a billing event; it is an identity. Inference bought with your key is attributable to you, arrives from your organisation’s account, and lands in your provider-side logs. The same gap that exports a chat log also exports the means to generate convincing new ones. That is the thread running from this batch back through the Wukong CRM records and the October gateway cluster: the AI feature is, in storage terms, a credential vault and a transcript archive wearing a chat interface, and it is routinely shipped with the authorisation maturity of a side project.

The records exist in every database and match nothing

We have reported a scanner gap in nearly every batch this month, usually as an absence — no GHSA, no OSV entry, no ecosystem. This batch inverts that and lands somewhere more uncomfortable. We queried all twenty CVE IDs against OSV and against GitHub’s global advisory database. All twenty resolve in both. Every one returns HTTP 200 from api.osv.dev; every one has a GHSA (CVE-2026-111704 became GHSA-hgqj-chvw-c4p5, critical, published 18:34:54Z — seventeen minutes after the CVE).

Then look at what the records contain. In OSV, all twenty carry an affected block whose only range is of type GIT — a repository URL and a last_affected commit hash — and not one of the twenty carries a package name or ecosystem. On the GitHub side, all twenty advisories are type: unreviewed with an empty vulnerabilities array: no package, no affected version range, nothing for Dependabot to match. The CVE is indexed, searchable, scored, and completely inert against an SBOM or a lockfile.

The mechanism is honest rather than negligent: these products are not packages. You deploy EFAK as a tarball, 99AI and ChatOllama from a Docker image or a git checkout, StaffDeck and Cube-studio from a clone. There is no npm, PyPI, Maven or crates.io coordinate to record, so the only true affected identifier is the commit — which is exactly what OSV recorded. The consequence is still that a defender running a complete, correctly configured dependency-scanning pipeline over an estate containing all sixteen of these products gets zero findings from this batch, and the pipeline is not broken. It is answering the question it was built to answer. Which registry packages am I consuming is simply a different question from which applications am I running, and the self-hosted AI stack now lives almost entirely in the second.

What to do

  • Inventory by deployed application, not by dependency. If these sixteen products would not appear in your asset list, this batch is invisible to you regardless of tooling. Reconcile running container images and git-deployed services against the CVE IDs above by hand; nothing will do it for you this week.
  • Rotate every provider key that one of these platforms has ever held. EFAK, LangChat and StaffDeck all expose stored provider credentials to the wrong caller, and key exposure is not something you can confirm did not happen from the outside. Rotate at the provider, then check provider-side usage logs for inference you did not buy.
  • Treat the chat archive as a disclosure asset with a retention policy. The 99AI export is bounded by how much history exists. Transcripts, like logs, should age out — and prompts routinely contain the material people would never paste into a public form.
  • Audit your own stack for the two patterns, not the two CVEs. First: does any authorisation allowlist enumerate page routes while the REST equivalents fall through to authenticated? Second: can a handler be written with no guard and still serve traffic? Deny-by-default routing turns the 99AI class of defect from a silent exposure into a failed request in staging.
  • Do not let a documentation decorator stand in for a check. @ApiBearerAuth, its equivalents in other frameworks, and padlocks in generated docs are assertions about the published specification, not about the router. Grep for routes that have the annotation and no guard; it is a two-line query and it finds exactly this bug.
  • Assume no fix. VulnCheck advisories record an affected ceiling and no patched version. We verified three upstreams directly — EFAK, 99AI and StaffDeck all still carry the defective code on their default branches at the time of writing, and LangChat’s affected commit is its HEAD. Plan for compensating controls (network placement, an authenticating reverse proxy, key scoping) rather than for an upgrade.

Our verification was read-only and external. The batch size, the 18:17:38.950Z–18:17:56.980Z publication window, the assigner, the Deferred status, and every CVE ID, CVSS 4.0 score and CWE above were read first-hand from the NVD 2.0 API. The AI subset was selected by us from the 122 records and is a judgement call, not a vendor classification. Severity, credit and affected ceilings were read from VulnCheck’s advisory pages; the technical writeups credited to hieuPenguinnn were read in full. The EFAK filter chain, the 99AI controller, the LangChat controller and login comparison, the DataBuff configuration file and the StaffDeck signer and settings default were each read directly from the named repositories at the pinned tag or commit and are quoted verbatim; default-branch state was checked separately where stated. The OSV and GitHub advisory results are our own measurements, taken at the time of writing. We installed nothing, deployed none of these products, sent no request to any third-party instance, and ran no exploit; the reporter states the same of their own work on several of these findings. We omit request bodies and parameter values that would make any of this directly reusable.

Sources: