MCP-AUTHZ-03 — Least Privilege Tool Design#
Level: L2 Domain: AUTHZ
Requirement#
MCP tools MUST be designed with the principle of least privilege, implementing minimal required permissions, avoiding permission aggregation, separating read from write operations, and providing granular operation-specific tools rather than general-purpose tools with broad capabilities.
Rationale#
Tools with excessive privileges create unnecessary risk surfaces. A single vulnerability in an over-privileged tool can compromise entire systems. Research shows 70% of MCP tools request more permissions than needed for their stated purpose, with common anti-patterns including combined read/write tools, tools that accept arbitrary commands, and tools with administrative capabilities enabled by default. Real incidents include ransomware deployment through file management tools (CVE-2025-61492) and data exfiltration through overly permissive query tools.
Scope#
Applies to:
- Tool design and architecture decisions
- Permission requests and capability declarations
- Default tool configurations
- Tool composition and chaining
- Error handling and fallback behaviors
- All tool types (filesystem, network, execution, database)
Does NOT apply to:
- Explicitly designated administrative tools (must be clearly marked)
- Emergency break-glass procedures (must be audited)
- Development/debugging tools (must not be in production)
Required Evidence#
Tool permission analysis: Documentation of minimal permissions per tool
tools: # Good: Separated, minimal permissions read_config: purpose: "Read application configuration" permissions: - fs:read:/config/*.json cannot: - write_files - execute_commands - network_access list_users: purpose: "List user accounts" permissions: - db:select:users:id,name,email cannot: - modify_data - access_passwords - cross_table_joins # Bad: Combined, excessive permissions file_manager: # ANTI-PATTERN permissions: - fs:read:/** - fs:write:/** - fs:delete:/** - fs:execute:/**Privilege separation code: Implementation showing separated operations
Test results:
- Read tool cannot perform writes
- Write tool cannot read arbitrary files
- Query tool cannot modify data
- Limited tool cannot escalate privileges
- Tool chaining cannot bypass restrictions
Verification Guidance#
Static (code/config review)#
Look for:
# Python example of least privilege tool design
# GOOD: Separated, specific tools
class ConfigReader:
"""Read-only access to configuration files."""
ALLOWED_PATHS = ['/etc/app/config.json', '/etc/app/settings.yaml']
def read_config(self, filename: str) -> dict:
if filename not in self.ALLOWED_PATHS:
raise PermissionError(f"Not allowed to read {filename}")
# Can only read, cannot write
with open(filename, 'r') as f:
return json.load(f)
class LogWriter:
"""Write-only access to log directory."""
LOG_DIR = '/var/log/app/'
def append_log(self, message: str, level: str = 'info'):
# Can only append to logs, cannot read or delete
log_file = os.path.join(self.LOG_DIR, f"{date.today()}.log")
# Validate we're writing to log directory
if not os.path.abspath(log_file).startswith(self.LOG_DIR):
raise PermissionError("Path traversal attempt")
# Append-only mode
with open(log_file, 'a') as f:
f.write(f"[{level.upper()}] {datetime.now()}: {message}\n")
class UserLister:
"""Read-only access to user list (no sensitive data)."""
def list_users(self, filters: dict = None) -> list:
# Only SELECT, only specific columns
query = "SELECT id, username, created_at FROM users WHERE active = true"
# No raw SQL injection possible
if filters:
if 'department' in filters:
query += " AND department = %s"
params = [filters['department']]
else:
params = []
# Cannot see passwords, tokens, or PII
with self.get_readonly_connection() as conn:
cursor = conn.cursor()
cursor.execute(query, params)
return cursor.fetchall()
# BAD: Overprivileged combined tool (ANTI-PATTERN)
class FileManager: # DO NOT USE THIS PATTERN
"""Excessive privileges in single tool."""
def execute_file_operation(self, operation: str, path: str, content: str = None):
# Too many capabilities in one tool
if operation == 'read':
return open(path).read() # Can read any file
elif operation == 'write':
open(path, 'w').write(content) # Can write anywhere
elif operation == 'delete':
os.remove(path) # Can delete anything
elif operation == 'execute':
subprocess.run(path) # Can execute anything// TypeScript example with privilege separation
// GOOD: Minimal, specific interfaces
interface ReadOnlyFileAccess {
readFile(path: string): Promise<Buffer>;
listDirectory(path: string): Promise<string[]>;
// No write, delete, or execute methods
}
interface AppendOnlyLogger {
appendLog(message: string, level: LogLevel): Promise<void>;
// No read, update, or delete methods
}
interface QueryOnlyDatabase {
select<T>(table: string, columns: string[], where?: Conditions): Promise<T[]>;
// No insert, update, delete, or DDL methods
}
// Implementation with strict validation
class SecureConfigReader implements ReadOnlyFileAccess {
private readonly allowedPaths = new Set([
'/app/config/settings.json',
'/app/config/features.json'
]);
async readFile(path: string): Promise<Buffer> {
const normalized = path.normalize(path);
if (!this.allowedPaths.has(normalized)) {
throw new UnauthorizedError(`Cannot read ${path}`);
}
// Read-only file handle
const handle = await fs.open(normalized, 'r');
try {
return await handle.readFile();
} finally {
await handle.close();
}
}
async listDirectory(path: string): Promise<string[]> {
// Can only list config directory
if (!path.startsWith('/app/config/')) {
throw new UnauthorizedError(`Cannot list ${path}`);
}
return await fs.readdir(path);
}
}
// GOOD: Separate tools for different privilege levels
class ToolRegistry {
// Public read operations
@RequireScope('public:read')
getPublicInfo(): PublicInfoReader {
return new PublicInfoReader();
}
// Authenticated read operations
@RequireScope('user:read')
getUserData(userId: string): UserDataReader {
return new UserDataReader(userId); // Scoped to single user
}
// Write operations require higher privilege
@RequireScope('user:write')
getUserUpdater(userId: string): UserUpdater {
return new UserUpdater(userId); // Can only update, not delete
}
// Admin operations clearly separated
@RequireScope('admin:write')
@RequireAuditLog
@RequireMFA
getAdminTool(): AdminTool {
return new AdminTool();
}
}Red flags:
- Tools that combine read and write operations
- Generic “execute command” or “run query” tools
- Tools with default admin/root privileges
- Fallback to higher privileges on error
- Tools that accept raw/unvalidated input paths
- Missing operation-specific access controls
Dynamic (behavioral verification)#
Test cases:
- Operation separation: Read tool attempting write → Denied
- Privilege escalation: Chaining tools to gain admin access → Blocked
- Scope creep: Tool accessing resources outside declared scope → Failed
- Default deny: Tool accessing undeclared resource type → Rejected
- Minimal exposure: Enumerate tool capabilities → Only necessary operations listed
- Privilege downgrade: Admin tool used for read → Uses minimal privileges
Expected behavior:
- Each tool performs exactly one type of operation
- No privilege escalation through tool combination
- Clear error when exceeding tool capabilities
- Tools fail safely without elevated fallbacks
- Audit logs show privilege checks
Mappings#
- CWE: CWE-250 (Execution with Unnecessary Privileges), CWE-269 (Improper Privilege Management), CWE-271 (Privilege Dropping/Lowering Errors)
- OWASP Top 10: A01:2021 – Broken Access Control, A04:2021 – Insecure Design
- OWASP MCP Top 10: MCP-03 (Authorization Bypass), MCP-07 (Excessive Tool Permissions)
- OWASP ASVS: V4.1.1 (Verify principle of least privilege), V1.4.1 (Verify all components run with least privilege)
Exceptions & Compensating Controls#
NOT_APPLICABLE when:
- Single-purpose server with one operation type
- All operations require identical privilege level (rare)
- Purely computational tools with no I/O
Compensating controls:
- Runtime privilege dropping after authentication
- Mandatory access control (MAC) systems (SELinux, AppArmor)
- Just-in-time (JIT) privilege elevation with MFA
- Time-boxed privilege grants with automatic expiration
Risk acceptance:
- May combine related read operations in L1
- MUST separate read/write/execute in L2+
- L3 requires granular operation-specific tools
References#
- 70% of MCP tools over-privileged by design (arXiv:2506.13538)
- Ransomware via file management tool (CVE-2025-61492)
- Principle of Least Privilege: Saltzer & Schroeder, 1975
- NIST SP 800-53: AC-6 Least Privilege
- Tool privilege escalation chains (OWASP MCP Security Guide)