Category: AI System Admin

  • AI System Admin WordPress Plugin Security Review

    Key Security Features

    • Fail-Closed Architecture — Default deny and explicit allow only
    • 7-Gate Permission System — Multiple independent security checks
    • Full OAuth 2.0 Compliance — PKCE, refresh tokens, and revocation
    • AES-256-CBC Token Encryption — Industry-standard cryptography
    • Scope-Based Access Control — Granular permission model
    • OAuth Isolation — Prevents tokens from accessing WordPress core API
    • Comprehensive Audit Logging — Full transparency
    • Rate Limiting — Prevents abuse

    OAuth 2.0 Security Architecture

    Authentication Flow

    The plugin implements an OAuth 2.0 flow initiated in Claude Desktop. When a user authorizes access:

    1. The browser redirects to WordPress’s authorize endpoint.
    2. The user approves scopes in the WordPress admin interface.
    3. WordPress issues a 10-minute authorization code.
    4. Claude Desktop exchanges the code for access (1 hr) and refresh (30 days) tokens using PKCE verification.
    5. Tokens are encrypted with AES-256-CBC before storage.

    Security strengths include:

    • Mandatory PKCE (prevents code interception)
    • Short-lived access tokens
    • Single-use authorization codes
    • CSRF protection via state parameter
    • Redirect URI validation

    Scope System

    Six distinct scopes define access levels:

    ScopeRisk LevelAccess
    readLowFile system (read_file, list_files, search_files)
    databaseLow–MediumSELECT-only queries
    memoryLowIsolated storage
    abilitiesMedium–HighWordPress abilities
    adminCriticalWildcard full access
    claudeaiCriticalFull MCP access

    Scopes are validated at authorization, token creation, execution, and MCP server level—a true defense-in-depth model.


    Token Encryption

    Algorithm: AES-256-CBC
    Key Derivation: PBKDF2-SHA256 (10,000 iterations)
    Key Material: WordPress salts (AUTH_KEY, SECURE_AUTH_KEY, LOGGED_IN_KEY, NONCE_KEY)
    Format: v1:base64(IV + ciphertext)

    Uses a unique IV per encryption, timing-safe comparisons, and versioning for future upgrades.


    Critical Security Feature: OAuth Token Isolation

    Unlike standard WordPress authentication, OAuth tokens never call wp_set_current_user().
    They only access plugin-specific endpoints—never core WordPress APIs.

    Result:

    • No access to admin panels
    • No privilege escalation or lateral movement


    Ability Permissions System

    7-Gate Security Model

    Each ability execution must pass through seven independent gates:

    1. Abilities API enabled?
    2. Ability registered in WordPress?
    3. Listed in permission registry?
    4. Admin enabled?
    5. Rate limits satisfied?
    6. User capability valid?
    7. (Critical only) Admin approval present?

    Default = DENY.
    Every denial is logged with detailed reasons.


    Permission Database Schema

    Dedicated database table includes:

    • Ability name (unique, indexed)
    • Enabled status & risk level
    • Rate limits (per day/hour)
    • Approval requirements (user/admin)
    • WordPress capability required
    • Custom validator function
    • Audit trail (execution count, timestamp)

    Rate Limiting

    Per-ability limits (daily + hourly) enforced via database queries.
    Strengths:

    • Fine-grained control
    • Accurate counters

    Input Validation

    Each permission can define a custom validator function, enforcing a fail-fast approach.
    Validation occurs before execution and is fully logged.


    Risk Level System

    LevelDescriptionApproval
    LowRead-only (no side effects)User approval
    MediumReversible writes (e.g., send-email)User approval
    HighSignificant writes (e.g., update-settings)User + higher limits
    CriticalDestructive actions (e.g., delete-user, SQL)Real-time admin approval

    Security Audit Logs

    Logging Architecture

    Logs record:

    • Permission checks (allowed/denied)
    • Ability executions
    • OAuth authorization events
    • Token generation, refresh, revocation
    • Rate limit violations
    • MCP tool executions

    Every event includes user, IP, timestamp, and reason. Logs are accessible via the WordPress admin.


    Threat Model Analysis

    1. Stolen OAuth Token

    • Risk: Low
    • Mitigation: Short expiration (1 hr), scoped tokens, no core API access, rate limits, full logging

    2. Malicious Ability Registration

    • Risk: Medium
    • Mitigation: Admin-only registration, approval with reasoning, full logging

    3. Rate Limit Bypass

    • Risk: Low
    • Mitigation: Database-enforced rate limits, per-ability counters

    4. Scope Escalation

    • Risk: Very Low
    • Mitigation: Cryptographically embedded scopes, validation on every tool call

    Conclusion

    The AI System Admin Plugin showcases security architecture built with defense-in-depth principles.

    Highlights:

    • Fail-Closed Permission Model
    • OAuth Token Isolation
    • Comprehensive Logging
    • Granular Scopes and Validation

    The standout achievement is OAuth token isolation, which prevents WordPress core access and drastically reduces potential attack surface.

  • MCP Bridge with AI System Admin

    Copy the bridge file code below and create a wordpress-mcp-bridge.js file .

    Instructions are below the code

    WordPress MCP Bridge Code
    wordpress-mcp-bridge.js
    #!/usr/bin/env node
    
    const https = require('https');
    
    const WP_SITE_URL = 'YOUR_SITE_URL';
    const CLIENT_ID = 'YOUR_CLIENT_ID';
    const CLIENT_SECRET = 'YOUR_CLIENT_SECRET';
    
    const WP_MCP_URL = `${WP_SITE_URL}/wp-json/ai-system-admin-mcp/v1/mcp`;
    const TOKEN_URL = `${WP_SITE_URL}/wp-json/ai-system-admin-mcp/v1/oauth/token`;
    
    let accessToken = null;
    let tokenExpiry = null;
    let buffer = '';
    
    async function getAccessToken() {
      if (accessToken && tokenExpiry && Date.now() < tokenExpiry) {
        return accessToken;
      }
    
      return new Promise((resolve, reject) => {
        const postData = new URLSearchParams({
          grant_type: 'client_credentials',
          client_id: CLIENT_ID,
          client_secret: CLIENT_SECRET
        }).toString();
    
        const options = {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': Buffer.byteLength(postData)
          }
        };
    
        const req = https.request(TOKEN_URL, options, (res) => {
          let data = '';
    
          res.on('data', (chunk) => {
            data += chunk;
          });
    
          res.on('end', () => {
            try {
              const response = JSON.parse(data);
              if (response.access_token) {
                accessToken = response.access_token;
                tokenExpiry = Date.now() + ((response.expires_in - 300) * 1000);
                resolve(accessToken);
              } else {
                reject(new Error(`OAuth error: ${JSON.stringify(response)}`));
              }
            } catch (e) {
              reject(new Error(`Failed to parse token response: ${e.message}`));
            }
          });
        });
    
        req.on('error', (e) => {
          reject(new Error(`OAuth request failed: ${e.message}`));
        });
    
        req.write(postData);
        req.end();
      });
    }
    
    process.stdin.setEncoding('utf8');
    process.stdin.on('data', (chunk) => {
      buffer += chunk;
    
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';
    
      lines.forEach(line => {
        if (line.trim()) {
          try {
            const request = JSON.parse(line);
            handleRequest(request);
          } catch (e) {
            sendError(null, -32700, 'Parse error', e.message);
          }
        }
      });
    });
    
    function handleRequest(request) {
      const { id, method, params } = request;
    
      if (method === 'initialize') {
        sendResponse(id, {
          protocolVersion: '2024-11-05',
          capabilities: {
            tools: {}
          },
          serverInfo: {
            name: 'wordpress-mcp',
            version: '1.0.0'
          }
        });
        return;
      }
    
      if (method === 'tools/list') {
        forwardToWordPress(request, id);
        return;
      }
    
      if (method === 'tools/call') {
        forwardToWordPress(request, id);
        return;
      }
    
      sendError(id, -32601, 'Method not found');
    }
    
    async function forwardToWordPress(request, id) {
      try {
        const token = await getAccessToken();
        const postData = JSON.stringify(request);
    
        const options = {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Content-Length': Buffer.byteLength(postData),
            'Authorization': `Bearer ${token}`
          }
        };
    
        const req = https.request(WP_MCP_URL, options, (res) => {
          let data = '';
    
          res.on('data', (chunk) => {
            data += chunk;
          });
    
          res.on('end', () => {
            try {
              const response = JSON.parse(data);
              sendResponse(id, response.result || response);
            } catch (e) {
              sendError(id, -32603, 'Internal error', `Failed to parse WordPress response: ${e.message}`);
            }
          });
        });
    
        req.on('error', (e) => {
          sendError(id, -32603, 'Internal error', `Request to WordPress failed: ${e.message}`);
        });
    
        req.write(postData);
        req.end();
      } catch (e) {
        sendError(id, -32603, 'OAuth error', e.message);
      }
    }
    
    function sendResponse(id, result) {
      const response = {
        jsonrpc: '2.0',
        id: id,
        result: result
      };
      process.stdout.write(JSON.stringify(response) + '\n');
    }
    
    function sendError(id, code, message, data = null) {
      const response = {
        jsonrpc: '2.0',
        id: id,
        error: {
          code: code,
          message: message,
          ...(data && { data })
        }
      };
      process.stdout.write(JSON.stringify(response) + '\n');
    }
    
    console.error('WordPress MCP Bridge started');
    console.error('Connecting to:', WP_MCP_URL);
    console.error('OAuth endpoint:', TOKEN_URL);

    Global Setup (Available Everywhere)

    To set up WordPress MCP globally so it’s available in all your Claude Code sessions regardless of which folder you’re in, first copy the template bridge file to a permanent location outside any project (like
    ~/mcp-bridges/wordpress-mcp-bridge.js), then edit it to replace the three placeholder values with your actual WordPress site URL and OAuth credentials from your WordPress plugin settings.

    Once configured, run claude mcp add -s user wordpress node ~/mcp-bridges/wordpress-mcp-bridge.js to register it with Claude Code.

    Now whenever you start Claude from any directory, the WordPress connection will be available because it’s saved in your global
    ~/.claude.json configuration file.

    Project-Only Setup (Specific Folder)

    To set up WordPress MCP for just one project folder, navigate to your project directory and create a scripts/mcp/
    folder inside it, then copy the template bridge there and configure it with your WordPress credentials. Instead of
    using the -s user flag, run claude mcp add -s project wordpress node scripts/mcp/wordpress-bridge.js which
    creates a .mcp.json file in your project root directory.

    This means the WordPress MCP connection will only work when you open Claude Code from within that specific project folder—if you open Claude from a different directory,
    it won’t have access to this WordPress connection. This is useful when working with teams (you can commit .mcp.json to git) or when different projects need to connect to different WordPress sites.

    Manual Configuration is Required

    Claude Code cannot automatically find your bridge file because:

    1. Security: It shouldn’t auto-execute random scripts it finds
    2. Credentials: The bridge needs credentials that only you know
    3. Flexibility: You might have multiple bridges with different names/locations
    4. Explicit control: You decide which MCP servers to enable The Easiest Way (One Command) The simplest approach is to use the claude mcp add command – it does all the config file editing for you: Global (available everywhere) claude mcp add -s user wordpress node ~/mcp-bridges/wordpress-mcp-bridge.js Or project-only cd /your/project
      claude mcp add -s project wordpress node scripts/mcp/wordpress-bridge.js This command automatically:
    • Adds the correct JSON to the config file
    • Sets the proper format and structure
    • Validates the configuration
    • No need to manually edit JSON

    You must configure once using claude mcp add, then everything else is automatic.

    Summary

    StepYou Do ManuallyClaude Code Does Automatically
    1. Create bridge file✓ Yes✗ No
    2. Add credentials✓ Yes✗ No
    3. Register with claude mcp add✓ Yes✗ No
    4. Start bridge on launch✗ No✓ Yes
    5. Connect via MCP✗ No✓ Yes
    6. Authenticate with WordPress✗ No✓ Yes (uses credentials in bridge)
    7. Load available tools✗ No✓ Yes