
SonicWall Capture Labs threat research team became aware of the threat CVE-2026-69098, assessed its impact, and developed mitigation measures. The flaw, also known as the kotaemon check_connection Unauthenticated Deserialization Remote Code Execution, is a critical vulnerability affecting kotaemon (Cinnamon/kotaemon), an open-source Gradio-based retrieval-augmented generation chat application, in all versions up to and including 0.12.0. It lets an unauthenticated attacker run arbitrary code on the server with a single HTTP POST to the model test-connection endpoint, because a client-supplied model specification is merged over the stored one and handed to a deserializer with its class allowlist disabled, instantiating any Python object the request names. Classified under CWE-502 (Deserialization of Untrusted Data) and rated CVSS 9.8 (Critical) under v3.1, with the CNA assigning v4.0 a score of 9.3, it was reported by HK4zCzi and assigned by VulnCheck, published on August 4, 2026. Its EPSS score is 0.91% (59.5th percentile). The severity reaches 9.8 because the optional login screen only hides interface tabs: the Gradio event API is served with no authentication, so any client that can reach the port can call the endpoint. No patched release is available, so administrators should restrict network access to the service until a fix ships.
kotaemon is an open-source retrieval-augmented generation application that lets users chat with their documents through pluggable language-model, embedding, and reranking providers. It is built on the Gradio web framework, is commonly self-hosted, and exposes its interface and HTTP API on TCP 7860. Its model-management screen offers a Test connection action that validates a provider configuration before it is saved.

The root cause is in the test-connection handler. In libs/ktem/ktem/llms/ui.py, check_connection parses the client-supplied specification as YAML, merges it over the stored model specification with a dictionary update, and passes the result to the theflow library's deserialize with safe=False. The YAML parsing itself is safe and builds only plain data; the flaw is that the merge lets a client key replace the stored __type__, and the deserializer is then invoked with its class allowlist turned off.

The deserializer treats any dictionary that carries a __type__ key as an object to construct. It imports the dotted path named in __type__, recursively deserializes the remaining keys into constructor arguments, and calls the class with them. Because the argument values are built before the outer object, a nested __type__ is instantiated first, which means a command runs during deserialization rather than after it.

The import is performed by import_dotted_string. The library ships an allowlist mode: when safe is true, it resolves names only against an explicit allowed_modules mapping and rejects anything else. kotaemon calls it with safe=False, which takes the unrestricted branch and imports any module and attribute the string names. The defense the library provides is present but not used on this path.

No upstream fix has been released, but the correct pattern already exists in the same file. The sibling create_llm and save_llm handlers, which persist a specification instead of instantiating it immediately, overwrite __type__ with a server-chosen value after parsing the client YAML, so a client-supplied type is ignored. check_connection, the one handler that constructs the object at once, omits that step, and the same omission is repeated in the embedding and reranking test-connection handlers. Pinning __type__ server-side as the sibling handlers do, together with re-enabling the deserializer allowlist, is the remediation.

The following conditions must be met for successful exploitation of CVE-2026-69098:
Exploiting CVE-2026-69098 needs no credentials and no special tooling. The attacker sends a single HTTP POST to the test-connection endpoint with a JSON body carrying a registered model name and a specification whose __type__ field the attacker controls. The core primitive is direct: setting __type__ to a callable such as subprocess.check_output, with an argument list, runs an operating-system command while the deserializer builds the object, before the handler does anything else. The command executes with the privileges of the kotaemon process. Because the deserializer recurses into every field, the attacker's callable can also be nested inside another field's value, so the command still runs when the outer object is a stored model class that would otherwise reject extra arguments.
The publicly demonstrated proof of concept adds one optional step to make the result visible. It sets the outer __type__ to unittest.mock.Mock, a standard-library testing class repurposed as a gadget because it accepts any arguments and is callable, and nests subprocess.check_output inside the mock's return_value. The handler's connection test then calls the deserialized object and echoes what it returns, so the command's standard output comes back in the response and a blind execution becomes one whose result is read straight from the reply. The wrapper also absorbs the stored specification's leftover fields, such as the model name and API key. It is a convenience for observing output, not a requirement for code execution.

With that payload, the reply carries the text Connection success. Got response: followed by the command output, for example the result of id, so a successful attack is visible in the response itself. A payload that omits the mock still runs the command; the endpoint simply reports a failed connection and returns nothing. The same deserializer backs all three test-connection handlers, for language models, embeddings, and rerankings, so each is independently exploitable.
Video Demonstration
| Component | Value | Purpose |
|---|---|---|
| Target Endpoint | POST /call/check_connection | Gradio event handler that instantiates the submitted model specification at once |
| Transport | plaintext HTTP on TCP 7860 | Default Gradio listener, with no TLS unless explicitly configured |
| Authentication | none | The event API is served without an authentication provider |
| Model Name | a registered name such as claude | Required to reach the deserializer; pre-registered on a default install |
| Injection Parameter | __type__ in the model specification | Client key that overwrites the stored class after the merge |
| Execution Primitive | subprocess.check_output with arguments | Instantiated and called during deserialization; runs the command (the core RCE) |
| Reflection Wrapper (optional) | unittest.mock.Mock in return_value | Public PoC technique to return the command output; not required for execution |
| Success Oracle | event-stream Connection success. Got response: | Command output echoed back in the response |
To ensure SonicWall customers are prepared for any exploitation that may occur due to this vulnerability, the following signature has been released:
| Signature ID | Signature Name |
|---|---|
| IPS: 22563 | kotaemon check_connection Insecure Deserialization |
The risks posed by CVE-2026-69098 can be mitigated or eliminated with the following measures:
The vulnerability was reported by HK4zCzi through the kotaemon project issue tracker and was assigned CVE-2026-69098 by VulnCheck as the CNA, published on August 4, 2026. No fixed release has been issued as of this writing.
Third-party vulnerability database mirrors:
Share This Article

An Article By
An Article By
Security News
Security News