FlawOpen

flawopen.com/AI Security/semantic-kernel-inmemory-vector-filter-cve-2026-26030

● CVE-2026-26030 · CVSS 9.9[1] · Critical
FlawOpen Security Research

Semantic Kernel CVE-2026-26030: Filter Code Injection

Prompt-controlled vector filters bypassed Semantic Kernel’s AST checks and reached Python eval. Upgrade the Python SDK to 1.39.4 or later.[1], [4]

CVSS vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H[1]
💡 Plain English Explainer (ELI5)

Imagine a librarian who accepts a search rule written on a card, checks that it uses a few familiar words, then asks a computer to run the card as a tiny program. An attacker who can influence the card can exploit gaps in that check. CVE-2026-26030 let a model-influenced filter cross from data lookup into code execution inside the application process.[4]

Core Concepts & Subsystem Terms

InMemoryVectorStore
Semantic Kernel’s in-process vector store. Its search operation can apply a filter to records before returning matches.[4]
Filter expression
A predicate that decides whether a stored record is eligible for a vector-search result. In the affected Python path, string filters were parsed as Python expressions and evaluated.[4]
Abstract Syntax Tree (AST)
A structured representation of parsed code. An AST validator can allow only the expression forms a filter needs, but an incomplete allowlist can leave paths to Python object introspection.[2], [4]
Prompt injection
Untrusted text influences an AI model to take an unintended action. Here, the security impact depends on the application exposing the vulnerable filter path to model-controlled input.[4]

Framework mappings

  • CWE reference for the weakness described in this guide: CWE-94

Step-by-Step Attack Flow

Step 1

The vulnerable filter path is deployed

The application uses Semantic Kernel’s Python package before 1.39.4, with InMemoryVectorStore filter functionality. Microsoft’s advisory rates the issue Critical (CVSS 3.1 9.9, CWE-94); the framework advisory says to upgrade to 1.39.4 or later.[1], [4]

Step 2

Untrusted content influences a model-selected filter

The documented attack requires a prompt-injection path that influences agent input and a Search Plugin backed by the default InMemoryVectorStore filter. A model-generated filter must not be treated as trusted code merely because it was returned through a typed tool call.[4]

Step 3

The filter becomes Python code

The pre-fix path parsed a filter as an AST and evaluated it with builtins removed, but its checks did not sufficiently constrain object traversal. The 1.39.4 implementation retains ast.Subscript for record-field access; it combines the node-type allowlist with a dangerous-attribute blocklist, lambda-parameter-only bare names, and an allowlist for calls. The upstream source does not reject subscripts by themselves, and eval with empty builtins is not a sandbox.[2], [4]

Step 4

Execution crosses into the application process

When the bypassed expression is evaluated during vector search, attacker-influenced logic can execute in the Python process hosting the agent. The resulting access is bounded by that process’s operating-system permissions, environment, credentials, and network reach.[4]

Step 5

Patch the parser boundary and investigate exposure

Upgrade semantic-kernel to 1.39.4 or later. The upstream patch adds dangerous-attribute checks and restricts bare names to the lambda parameter; Microsoft documents an AST-node allowlist, function allowlist, attribute blocklist, and name restriction. Review the affected deployment window for process, network, and credential-use anomalies.[1], [2], [3], [4]

Source Code: Flaw vs. Secure Implementation

The vulnerable pane is a representative pattern, not verbatim product source: a model-controlled value is interpolated into Python syntax before eval. The hardened pane focuses on the upstream 1.39.4 checks. Its configured AST allowlist intentionally includes ast.Subscript for record-field access; the patch combines that node allowlist with blocked dangerous attributes, lambda-only bare names, and allowed function or method calls. Empty builtins alone do not make eval a sandbox. Upgrade the package; do not copy this excerpt as a standalone validator.[2], [6], [4]

UNPATCHED FLAW
# Representative pre-fix pattern: model-controlled text changes the expression.
filter_text = f"lambda x: x.city == '{model_selected_city}'"
tree = ast.parse(filter_text, mode="eval")
code = compile(tree, filename="<filter>", mode="eval")
filter_fn = eval(code, {"__builtins__": {}}, {})  # nosec
HARDENED SECURE PATCH
def validate_filter(self, tree):
    # Focused upstream 1.39.4 checks; exception messages are shortened.
    # Top-level filters must be lambda expressions.
    if not (isinstance(tree, ast.Expression) and isinstance(tree.body, ast.Lambda)):
        raise VectorStoreOperationException("Filter must be a lambda expression")

    lambda_param_names = {arg.arg for arg in tree.body.args.args}
    for node in ast.walk(tree):
        if type(node) not in self.allowed_filter_ast_nodes:
            raise VectorStoreOperationException("AST node type is not allowed")

        if isinstance(node, ast.Attribute) and node.attr in self.blocked_filter_attributes:
            raise VectorStoreOperationException("Dangerous attribute is not allowed")

        if isinstance(node, ast.Name) and node.id not in lambda_param_names:
            raise VectorStoreOperationException("Only lambda parameter names are allowed")

        if isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name):
                func_name = node.func.id
            elif isinstance(node.func, ast.Attribute):
                func_name = node.func.attr
            else:
                func_name = None
            if func_name and func_name not in self.allowed_filter_functions:
                raise VectorStoreOperationException("Function is not allowed")

    code = compile(tree, filename="<filter>", mode="eval")
    func = eval(code, {"__builtins__": {}}, {})  # nosec

Engineering & System Hardening Checklist

Sources

← AI Security Hub Directory →