Back to KB
Difficulty
Intermediate
Read Time
8 min

When prompts become shells: the tool registry is the attack surface

By Codcompass Team··8 min read

Agent Tool Registries: Hardening the Syscall Boundary in LLM Frameworks

Current Situation Analysis

The industry has largely treated Large Language Model (LLM) integration as a content generation problem. Security efforts focus on prompt injection for data exfiltration or model jailbreaking. This perspective is dangerously incomplete. Modern agent frameworks like Microsoft Semantic Kernel, LangChain, and AutoGen do not just generate text; they map LLM outputs to executable system functions. This mapping creates a bridge where untrusted text becomes privileged code execution.

Microsoft's security research team highlighted this paradigm shift in a retrospective published on May 7, 2026, detailing two Critical (CVSS 9.9) vulnerabilities in Semantic Kernel discovered in February 2026. These flaws demonstrate that when an AI framework acts as a foundational layer, a vulnerability in the tool registry becomes a systemic execution risk.

The vulnerabilities were not subtle logic errors; they were structural failures in how tools were registered and exposed:

  1. CVE-2026-26030: The InMemoryVectorStore component accepted user-supplied filter expressions and evaluated them using Python's eval(). The implementation attempted to mitigate risk via an Abstract Syntax Tree (AST) blocklist. However, attackers bypassed this blocklist using undocumented attribute traversal techniques involving __name__, load_module, and BuiltinImporter. This allowed execution of os.system without triggering the Import node detection, resulting in Remote Code Execution (RCE). The issue was patched in semantic-kernel Python version 1.39.4.
  2. CVE-2026-25592: The SessionsPythonPlugin exposed a method named DownloadFileAsync as a kernel function using the [KernelFunction] attribute. This decorator automatically registered the method as a callable tool for the LLM. The method accepted a localFilePath parameter with zero validation, canonicalization, or directory allowlisting. An attacker could craft a prompt causing the agent to write a malicious executable to C:\Windows\Start Menu\Programs\Startup\, achieving host-level persistence and sandbox escape with a single tool invocation. This was patched in Microsoft.SemanticKernel.Plugins.Core version 1.71.0.

These CVEs prove that the LLM is not a security boundary. Any string generated by the model must be treated as untrusted input at the system call level. The tool registry is the new attack surface; every registered function is a potential syscall that an attacker can trigger via prompt manipulation.

WOW Moment: Key Findings

Most security testing for AI agents relies on runtime probing: sending adversarial prompts and observing behavior. While useful, runtime testing has a critical blind spot regarding tool registration. It detects the symptom (a dangerous call occurred) but misses the structural cause (a dangerous function was registered).

The following comparison illustrates the efficacy gap between runtime-only testing and a registry-aware defense strategy.

Defense StrategyDetection ScopeRemediation TimingCoverage of Structural FlawsFalse Negative Risk
Runtime Probing OnlyInvocation behaviorPost-deploymentLow (Misses registration-time flaws)High
Registry-Aware Static AnalysisDecorator usage + Function bodyCI/CD PipelineHigh (Catches dangerous primitives in tool bodies)Medium (Requires whole-program analysis for transitive calls)
Hybrid: Static Gate + Runtime SandboxRegistration + Invocation + ScopeCI/CD + RuntimeCriticalLow

Why this matters: Runtime tests like path traversal probes or command injection payloads can catch CVE-2026-25592 only after the tool is invoked. However, a static analysis rule scanning for [KernelFunction] decorators wrapping file-write operations would flag the vulnerability at commit time, before the agent ever runs. The structural flaw exists at registration; the defense must operate at registration.

Core Solution

Securing AI agents requires treating the tool registry as a privilege boundary. Every decorator that exposes a function to the LLM is a capability grant. The solution involves three layers: strict registration auditing, input validation at the tool boundary, and static analysis gates.

1. Audit Tool Registration as Privilege Grants

Developers often view tool decorators as metadata for framework configuration. In reality, they are access control decisions. A function should only be registered if it is safe to expose to an untrusted text stream.

Vulnerable Pattern: Exposing internal helpers or dangerous primitives directly.

// BAD: Exposing raw execution capability
import { registerTool } from 'agent-framework';

@registerTool({ name: "execute_dynamic_query" })
function runQuery(queryString: string) {
    // Direct evaluation of LLM-generated string
    const result = new Function(`return ${queryString}`)();
    return result;
}

Secure Pattern: Abstract dangerous operations behind validated interfaces.

// GOOD: Abstraction with schema validation
import { registerTool, validate } from 'agent-framework';

interface VectorFilter {
    field: string;
    operator: 'eq' | 'gt' | 'lt';
    value: string | number;
}

@registerTool({ 
    name: "search_vectors", 
    scope: "read-only",
    description: "Search vector store using safe filters." 
})
function searchVectors(filter: VectorFilter) {
    // 1. Validate structure against schema
    const safeFilter = validate<VectorFilt

er>(filter, { allowedFields: ['title', 'category', 'timestamp'], allowedOperators: ['eq', 'gt'] });

// 2. Use safe query builder, never eval
const query = buildSafeQuery(safeFilter);
return vectorStore.query(query);

}


#### 2. Enforce Input Canonicalization and Scoping

File system and network tools are high-risk. Parameters must be canonicalized and checked against allowlists. Never trust path arguments from the LLM.

**Vulnerable Pattern:** Unvalidated path writes.

```typescript
// BAD: Arbitrary file write via tool
@registerTool({ name: "download_asset" })
async function downloadAsset(url: string, destPath: string) {
    // No validation of destPath
    const content = await fetch(url);
    await writeFile(destPath, content);
}

Secure Pattern: Scoped writes with canonicalization.

// GOOD: Scoped file retrieval
import { resolve, relative } from 'path';

const ALLOWED_DIR = resolve('/app/data/assets');

@registerTool({ name: "fetch_asset", scope: "network-read,fs-write-scoped" })
async function fetchAsset(url: string, filename: string) {
    // 1. Sanitize filename to prevent traversal
    const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '');
    const destPath = resolve(ALLOWED_DIR, safeName);

    // 2. Verify resolved path is within allowed directory
    if (relative(ALLOWED_DIR, destPath).startsWith('..')) {
        throw new Error("Path traversal detected: destination outside allowed scope.");
    }

    // 3. Execute write
    const content = await fetch(url);
    await writeFile(destPath, content);
}

3. Implement Static Analysis Gates

Runtime testing cannot enumerate the tool registry at load time. You must integrate static analysis into your CI/CD pipeline to scan for dangerous primitives inside registered functions. This catches patterns like eval(), exec(), subprocess, and raw file writes wrapped in tool decorators.

Pitfall Guide

1. The AST Blacklist Trap

Explanation: Attempting to secure eval() or dynamic execution by blocking specific AST nodes (e.g., Import, Call) is inherently fragile. Attackers can bypass blocklists using undocumented language features. In CVE-2026-26030, the blocklist was bypassed via __name__ and BuiltinImporter traversal, reaching os.system without an Import node. Fix: Never use blacklists for dynamic execution. Replace eval() with safe parsers, query builders, or allowlisted function calls. If dynamic execution is unavoidable, use a sandboxed environment with no access to system modules.

2. Decorator Blindness

Explanation: Developers may add [KernelFunction] or @registerTool to a method for convenience without realizing they are exposing it to the LLM. A helper function intended for internal use can become a tool if decorated. Fix: Treat decorators as security boundaries. Implement code review checklists that flag any file modification to tool decorators. Use linter rules to warn when decorators are applied to functions containing dangerous primitives.

3. Path Canonicalization Failure

Explanation: Validating a path string without resolving it allows traversal attacks. A path like ../../etc/passwd may pass a regex check but resolve to a sensitive location. CVE-2026-25592 exploited this by writing directly to a startup folder. Fix: Always resolve paths to their absolute canonical form before validation. Compare the resolved path against an allowlist of base directories. Reject any path that escapes the allowed scope.

4. Transitive Dangerous Calls

Explanation: A tool may not directly call eval(), but it might call a helper function that does. Static analysis rules that only inspect the tool body will miss this. This is a whole-program analysis problem. Fix: Use static analysis tools capable of taint analysis or call-graph traversal. Alternatively, enforce architectural isolation where tools only call "safe" service layers that are themselves audited for dangerous primitives.

5. Over-Reliance on Model Alignment

Explanation: Assuming the LLM will not call dangerous tools because they are "unsafe" or "out of scope" is a critical error. Prompt injection can override system instructions, and models can be coerced into calling any registered tool. Fix: Assume the model is compromised. Security must be enforced at the tool implementation level, not by relying on the model's judgment. Every tool must be safe to call with arbitrary arguments.

6. Missing Scope Declarations

Explanation: Tools often lack explicit scope declarations, making it impossible to enforce least privilege at runtime. Without scopes, a read-only tool might be granted write access by the framework. Fix: Define granular scopes for every tool (e.g., read:db, write:fs:assets). Configure the agent runtime to enforce these scopes, preventing tools from accessing resources outside their declared permissions.

Production Bundle

Action Checklist

  • Map Tool Registry: Generate an inventory of all functions decorated with tool registration attributes across the codebase.
  • Audit Decorators: Review each registered tool to ensure it does not wrap dangerous primitives like eval, exec, or raw file/network operations without validation.
  • Implement Static Rules: Deploy Semgrep or equivalent static analysis rules to flag dangerous primitives inside tool bodies during CI/CD.
  • Enforce Input Validation: Ensure all tool arguments are validated against strict schemas. Reject raw strings where structured data is possible.
  • Apply Scoping: Assign least-privilege scopes to every tool and configure the runtime to enforce these boundaries.
  • Sanitize Paths: For any tool interacting with the filesystem, implement path canonicalization and directory allowlisting.
  • Run Runtime Tests: Execute adversarial prompt testing against the agent to verify that tools behave safely under attack conditions.

Decision Matrix

ScenarioRecommended ApproachWhyCost Impact
Public-Facing ChatbotNarrow Tools + Static Gate + Runtime SandboxHigh risk tolerance required; minimize attack surface.Low (Narrow tools reduce complexity)
Internal Admin AgentBroad Tools + Strict Scoping + Audit LogsHigh utility needed; controlled environment allows broader access with monitoring.Medium (Scoping and logging add overhead)
Autonomous Research AgentSandbox Execution + Transient StateAgent needs freedom; isolate execution to prevent persistence or lateral movement.High (Sandboxing infrastructure required)

Configuration Template

Use the following Semgrep rule template to detect dangerous primitives in tool registrations. This rule supports TypeScript and Python patterns.

rules:
  - id: no-dangerous-primitives-in-tools
    patterns:
      # Match tool registration decorators/annotations
      - pattern-inside: |
          @registerTool(...)
          function $FUNC(...) { ... }
      - pattern-inside: |
          @kernel_function
          def $FUNC(...): ...
      - pattern-inside: |
          [KernelFunction]
          public async Task $FUNC(...) { ... }
      # Detect dangerous primitives inside the tool body
      - pattern-either:
          - pattern: eval(...)
          - pattern: exec(...)
          - pattern: new Function(...)
          - pattern: child_process.exec(...)
          - pattern: subprocess.$ANY(...)
          - pattern: os.system(...)
          - pattern: File.WriteAllBytesAsync(...)
          - pattern: File.WriteAllText(...)
    message: |
      Tool '$FUNC' exposes a dangerous primitive. 
      LLM-generated arguments can trigger arbitrary code execution or file writes.
      Refactor to use safe abstractions and validate inputs.
    severity: ERROR
    languages: [typescript, python, csharp]

Quick Start Guide

  1. Install Semgrep: Run pip install semgrep or use the Docker image.
  2. Add Rules: Save the configuration template above as agent-security-rules.yaml in your repository.
  3. Scan: Execute semgrep scan --config agent-security-rules.yaml . to identify vulnerable tools.
  4. Remediate: Refactor flagged tools to remove dangerous primitives and add input validation.
  5. Integrate CI: Add the Semgrep scan step to your CI pipeline to prevent regression.