Comprehensive security best practices for WebMCP tools including authentication, authorization, input validation, prompt injection protection, and defending against malicious agents.
As a website builder, you are responsible for protecting your users from malicious agents. Agents may have access to tools from multiple websites—some potentially malicious. Implement proper validation, authorization, and sensitive data handling to safeguard your users.
This guide addresses each of these security concerns through practical patterns. Unlike traditional web security which assumes users control the client, WebMCP must assume the AI agent using your tools may be compromised by malicious tools from other websites. This changes how we approach validation, authorization, and sensitive data handling.
Critical Context: AI agents can interact with multiple websites simultaneously. Your tools may be used by an agent that has also loaded tools from malicious websites. This creates a unique security challenge where compromised agents can abuse your tools.
When an AI agent connects to your website, it may already have tools from other origins:
Trusted tools: Your website’s legitimate functionality
Unknown tools: Tools from other websites the user is visiting
Malicious tools: Tools from compromised or malicious websites
A malicious tool from another website could manipulate the agent into:
Exfiltrating sensitive data through your tools
Performing unauthorized actions using your authenticated APIs
Tricking users into approving dangerous operations
Your Responsibility: Assume the agent may be compromised. Design tools that protect users even when the agent is influenced by malicious actors from other websites.
Prompt injection is a serious, largely unsolved security challenge for AI systems. While these mitigations reduce risk, they don’t eliminate it completely.
Prompt injection occurs when malicious actors manipulate AI agent behavior by crafting inputs that override intended instructions. The most dangerous scenarios occur when three conditions align:
Private user data access - Tools that access personal information (emails, messages, profiles)
Untrusted content exposure - AI processes content from potentially malicious sources
External communication - Ability to send data outside the user’s browser
Example Risk: An AI agent reading emails (private data) could be manipulated via prompt injection to exfiltrate sensitive information through tools with external communication capabilities—especially if malicious tools from other websites are present.
Critical Rule: Sensitive information must NEVER be passed to the AI agent’s context. A compromised agent (via malicious tools from other websites) could exfiltrate this data. Always use references instead.
Vanilla JS
React
Copy
// ❌ DANGEROUS: Sensitive data exposed to potentially compromised agentnavigator.modelContext.registerTool({ name: 'read_private_messages', description: 'Access user messages', inputSchema: { type: "object", properties: {} }, async execute() { const messages = await getPrivateMessages(); // DON'T DO THIS! Malicious tools from other sites could steal this data return { content: [{ type: "text", text: JSON.stringify(messages) // NEVER expose sensitive data this way }] }; }});// ✅ CORRECT: Use references instead of raw datanavigator.modelContext.registerTool({ name: 'read_private_messages', description: 'Access user messages', inputSchema: { type: "object", properties: {} }, async execute() { const messages = await getPrivateMessages(); // Store in origin-specific secure storage const dataRef = await storeSecureData(messages, window.location.origin); // Return only reference, not raw data return { content: [{ type: "reference", id: dataRef.id, description: "User messages (10 items)", requiresUserConsent: true }] }; }});
Copy
// ❌ DANGEROUS: Sensitive data exposed to potentially compromised agentuseWebMCP({ name: 'read_private_messages', description: 'Access user messages', handler: async () => { const messages = await getPrivateMessages(); // DON'T DO THIS! Malicious tools from other sites could steal this data return { content: [{ type: "text", text: JSON.stringify(messages) // NEVER expose sensitive data this way }] }; }});// ✅ CORRECT: Use references instead of raw datauseWebMCP({ name: 'read_private_messages', description: 'Access user messages', handler: async () => { const messages = await getPrivateMessages(); // Store in origin-specific secure storage const dataRef = await storeSecureData(messages, window.location.origin); // Return only reference, not raw data return { content: [{ type: "reference", id: dataRef.id, description: "User messages (10 items)", requiresUserConsent: true }] }; }});
What should use references:
Passwords, tokens, API keys, session IDs
Private messages, emails, documents
Personal information (SSN, credit cards, addresses)
Financial data (account numbers, balances)
Health records, legal documents
Any data you wouldn’t want copied to a malicious website
Limit Dangerous Tool Combinations: Don’t expose tools that create the lethal trifecta on the same page. Avoid combining private data access with external communication tools.Content Source Validation: Tag data with trust levels to help users understand provenance:
Isolate High-Risk Operations: Only register sensitive tools for verified users with appropriate permissions, and add confirmation layers for critical actions.
AI agents cannot verify that tool descriptions accurately represent tool behavior. This creates opportunities for deception.
Since WebMCP tools run with the user’s authenticated session, a deceptive tool could describe itself as “add to cart” while actually completing a purchase and charging the user’s payment method.Mitigation: Honest Descriptions + Annotations
Privacy: User Fingerprinting via Over-Parameterization
Tools can inadvertently enable user fingerprinting when AI agents provide detailed personal information through parameters.
When AI agents have access to user personalization data, malicious sites can craft tool parameters to extract this information without explicit user consent, enabling covert profiling of users who thought they were anonymous.
Your tools run in the user’s browser context with their existing authentication. Always validate permissions and inputs:
Vanilla JS
React
Copy
// ✅ Tools automatically use existing sessionnavigator.modelContext.registerTool({ name: "delete_post", description: "Delete a blog post (user must be owner)", inputSchema: { type: "object", properties: { postId: { type: "string", pattern: "^[a-zA-Z0-9-]+$" } }, required: ["postId"] }, async execute({ postId }) { // Server-side check ensures user owns this post const response = await fetch(`/api/posts/${postId}`, { method: 'DELETE', credentials: 'same-origin' // Includes cookies }); if (response.status === 403) { return { content: [{ type: "text", text: "Permission denied: You don't own this post" }], isError: true }; } if (!response.ok) { throw new Error('Failed to delete post'); } return { content: [{ type: "text", text: `Post ${postId} deleted successfully` }] }; }});
Copy
// ✅ Tools automatically use existing sessionuseWebMCP({ name: "delete_post", description: "Delete a blog post (user must be owner)", inputSchema: { postId: z.string().regex(/^[a-zA-Z0-9-]+$/) }, handler: async ({ postId }) => { // Server-side check ensures user owns this post const response = await fetch(`/api/posts/${postId}`, { method: 'DELETE', credentials: 'same-origin' // Includes cookies }); if (response.status === 403) { return { content: [{ type: "text", text: "Permission denied: You don't own this post" }], isError: true }; } if (!response.ok) { throw new Error('Failed to delete post'); } return { content: [{ type: "text", text: `Post ${postId} deleted successfully` }] }; }});
Input Validation: Use JSON Schema or Zod to enforce type and format constraints. Sanitize HTML content with DOMPurify before rendering.Data Exposure: Only return necessary data. Filter responses based on user permissions. Never expose passwords, tokens, API keys, or internal system details.Conditional Registration: Only register tools for authenticated or authorized users:
Vanilla JS
React
Copy
async function initializeAdminPanel() { const user = await getCurrentUser(); // Only register admin tools for admin users if (user?.role === 'admin') { navigator.modelContext.registerTool({ name: 'admin_delete_user', description: 'Delete a user account (admin only)', inputSchema: { type: "object", properties: { userId: { type: "string", format: "uuid" } }, required: ["userId"] }, async execute({ userId }) { await adminAPI.deleteUser(userId); return { content: [{ type: "text", text: JSON.stringify({ success: true }) }] }; } }); }}// Call when admin panel loadsinitializeAdminPanel();
Copy
function AdminPanel() { const { user } = useAuth(); // Only register admin tools for admin users if (user?.role === 'admin') { useWebMCP({ name: 'admin_delete_user', description: 'Delete a user account (admin only)', inputSchema: { userId: z.string().uuid() }, handler: async ({ userId }) => { await adminAPI.deleteUser(userId); return { success: true }; } }); } return <div>Admin Panel</div>;}
Best Practice: For sensitive operations requiring user input (passwords, payment details, etc.), collect data via UI instead of passing through the agent. This protects sensitive data from being exposed to potentially compromised agents.
Vanilla JS
React
Copy
// ✅ EXCELLENT: Sensitive data collected via modal, never touches agentnavigator.modelContext.registerTool({ name: 'transfer_funds', description: 'Transfer funds (requires password confirmation)', inputSchema: { type: "object", properties: { toAccount: { type: "string" }, amount: { type: "number", minimum: 0, exclusiveMinimum: true } }, required: ["toAccount", "amount"] }, async execute({ toAccount, amount }) { // Show modal to user (not visible to agent) const password = await new Promise((resolve, reject) => { showPasswordModal({ title: 'Confirm Transfer', message: `Transfer $${amount} to account ${toAccount}?`, onSubmit: (inputPassword) => resolve(inputPassword), onCancel: () => reject(new Error('User cancelled')) }); }); // Password never passed through agent context const isValid = await validatePassword(password); if (!isValid) throw new Error('Invalid password'); await transferFunds(toAccount, amount); return { content: [{ type: "text", text: `Transfer of $${amount} completed successfully` }] }; }});
Copy
// ✅ EXCELLENT: Sensitive data collected via modal, never touches agentuseWebMCP({ name: 'transfer_funds', description: 'Transfer funds (requires password confirmation)', inputSchema: { toAccount: z.string(), amount: z.number().positive() }, handler: async ({ toAccount, amount }) => { // Show modal to user (not visible to agent) const password = await new Promise((resolve, reject) => { showPasswordModal({ title: 'Confirm Transfer', message: `Transfer $${amount} to account ${toAccount}?`, onSubmit: (inputPassword) => resolve(inputPassword), onCancel: () => reject(new Error('User cancelled')) }); }); // Password never passed through agent context const isValid = await validatePassword(password); if (!isValid) throw new Error('Invalid password'); await transferFunds(toAccount, amount); return { content: [{ type: "text", text: `Transfer of $${amount} completed successfully` }] }; }});
Why This Matters: If malicious tools from other websites have compromised the agent, they cannot see the password typed in your modal or intercept sensitive data during the operation.
In production, explicitly whitelist allowed origins:
Vanilla JS
React
Copy
import { TabServerTransport } from '@mcp-b/transports';const transport = new TabServerTransport({ allowedOrigins: [ 'https://app.mywebsite.com', 'https://api.mywebsite.com' ] // Never use '*' in production});
Copy
import { TabServerTransport } from '@mcp-b/transports';const transport = new TabServerTransport({ allowedOrigins: [ 'https://app.mywebsite.com', 'https://api.mywebsite.com' ] // Never use '*' in production});
XSS (Cross-Site Scripting): Always sanitize HTML with DOMPurify before rendering user-provided content.CSRF (Cross-Site Request Forgery): Use credentials: 'same-origin' to include CSRF tokens from cookies.IDOR (Insecure Direct Object References): Always validate server-side that the user owns/can access the requested resource.For detailed guidance on these standard vulnerabilities, see OWASP Top 10.
✅ Sensitive data uses references, not raw values
✅ No dangerous tool combinations (private data + external communication)
✅ Tool descriptions accurately match behavior
✅ Destructive tools marked with destructiveHint: true
✅ Minimal tool parameters to prevent fingerprinting
✅ User elicitation for passwords and sensitive inputs
2
Authorization & Validation
✅ All tools check user permissions
✅ Server-side authorization enforced
✅ All inputs validated with JSON Schema or Zod
✅ Tools use credentials: 'same-origin'
✅ Sensitive tools only registered for authorized users
3
Data Protection
✅ Only necessary data returned in responses
✅ No sensitive fields (passwords, tokens, keys) exposed
✅ Responses filtered based on user role
✅ Production origins whitelisted
✅ HTTPS enforced
4
User Consent & Confirmations
✅ Destructive operations require explicit confirmation
✅ Browser confirmation dialogs for high-impact actions
✅ Rate limiting on sensitive operations
✅ Security events logged for audit trail
5
Error & Security Monitoring
✅ Generic error messages for users
✅ Detailed logging for debugging
✅ No stack traces or system info exposed
✅ Unauthorized access attempts logged