AI agents generate tool arguments dynamically. Unlike human-driven applications where inputs come from forms with known constraints, agent inputs are generated by a language model that can produce any string. Every tool must validate every argument.
A language model can generate:
../../etc/passwd)'; DROP TABLE users;--)http://169.254.169.254/metadata)amount: -1000)Your tools should handle all of these safely.
Verify argument types before processing:
function validateArgs(args: unknown, schema: JSONSchema): ValidationResult {
const result = validate(args, schema);
if (!result.valid) {
return { error: `Invalid arguments: ${result.errors.join(', ')}` };
}
return { valid: true };
}
Use the same JSON Schema definitions that describe the tool's parameters to validate incoming arguments.
For tools that accept file paths:
function validatePath(inputPath: string, allowedBase: string): string {
const resolved = path.resolve(inputPath);
// Prevent path traversal
if (!resolved.startsWith(allowedBase)) {
throw new Error('Path outside allowed directory');
}
// Block sensitive files
const blocked = ['.env', 'credentials', 'secrets', '/etc/shadow'];
if (blocked.some(b => resolved.includes(b))) {
throw new Error('Access to sensitive file blocked');
}
return resolved;
}
For tools that accept database queries:
function validateQuery(query: string): void {
// Only allow SELECT statements
const trimmed = query.trim().toUpperCase();
if (!trimmed.startsWith('SELECT')) {
throw new Error('Only SELECT queries are allowed');
}
// Block dangerous keywords
const blocked = ['DROP', 'DELETE', 'INSERT', 'UPDATE', 'TRUNCATE', 'ALTER', 'GRANT'];
for (const keyword of blocked) {
if (trimmed.includes(keyword)) {
throw new Error(`Query contains blocked keyword: ${keyword}`);
}
}
}
Better yet, use parameterized queries and only allow the agent to provide parameter values, not query structure.
For tools that make HTTP requests:
function validateUrl(url: string, allowedDomains: string[]): void {
const parsed = new URL(url);
// Block internal IPs
if (isInternalIP(parsed.hostname)) {
throw new Error('Internal IP addresses blocked');
}
// Block non-HTTPS
if (parsed.protocol !== 'https:') {
throw new Error('Only HTTPS URLs allowed');
}
// Check domain allowlist
if (!allowedDomains.some(d => parsed.hostname.endsWith(d))) {
throw new Error('Domain not in allowlist');
}
}
For tools that accept amounts, counts, or other numbers:
function validateAmount(amount: number): void {
if (typeof amount !== 'number' || isNaN(amount)) {
throw new Error('Amount must be a number');
}
if (amount < 0) {
throw new Error('Amount cannot be negative');
}
if (amount > MAX_AMOUNT) {
throw new Error(`Amount exceeds maximum (${MAX_AMOUNT})`);
}
}
Input validation at the tool level is the last line of defense. It should complement, not replace, policy-level argument validation. The policy engine catches many issues before the tool is called. Tool-level validation catches anything the policy missed.
Explore more guides on AI agent safety, prompt injection, and building secure systems.
View All Guides