When prompts become shells: the tool registry is the attack surface
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:
- CVE-2026-26030: The
InMemoryVectorStorecomponent accepted user-supplied filter expressions and evaluated them using Python'seval(). 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, andBuiltinImporter. This allowed execution ofos.systemwithout triggering theImportnode detection, resulting in Remote Code Execution (RCE). The issue was patched insemantic-kernelPython version1.39.4. - CVE-2026-25592: The
SessionsPythonPluginexposed a method namedDownloadFileAsyncas a kernel function using the[KernelFunction]attribute. This decorator automatically registered the method as a callable tool for the LLM. The method accepted alocalFilePathparameter with zero validation, canonicalization, or directory allowlisting. An attacker could craft a prompt causing the agent to write a malicious executable toC:\Windows\Start Menu\Programs\Startup\, achieving host-level persistence and sandbox escape with a single tool invocation. This was patched inMicrosoft.SemanticKernel.Plugins.Coreversion1.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 Strategy | Detection Scope | Remediation Timing | Coverage of Structural Flaws | False Negative Risk |
|---|---|---|---|---|
| Runtime Probing Only | Invocation behavior | Post-deployment | Low (Misses registration-time flaws) | High |
| Registry-Aware Static Analysis | Decorator usage + Function body | CI/CD Pipeline | High (Catches dangerous primitives in tool bodies) | Medium (Requires whole-program analysis for transitive calls) |
| Hybrid: Static Gate + Runtime Sandbox | Registration + Invocation + Scope | CI/CD + Runtime | Critical | Low |
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
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Public-Facing Chatbot | Narrow Tools + Static Gate + Runtime Sandbox | High risk tolerance required; minimize attack surface. | Low (Narrow tools reduce complexity) |
| Internal Admin Agent | Broad Tools + Strict Scoping + Audit Logs | High utility needed; controlled environment allows broader access with monitoring. | Medium (Scoping and logging add overhead) |
| Autonomous Research Agent | Sandbox Execution + Transient State | Agent 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
- Install Semgrep: Run
pip install semgrepor use the Docker image. - Add Rules: Save the configuration template above as
agent-security-rules.yamlin your repository. - Scan: Execute
semgrep scan --config agent-security-rules.yaml .to identify vulnerable tools. - Remediate: Refactor flagged tools to remove dangerous primitives and add input validation.
- Integrate CI: Add the Semgrep scan step to your CI pipeline to prevent regression.
