mcpField
N
n8n-security-testing

n8n-security-testing

github.com/proffesor-for-testingセキュリティ519171 回インストール今日更新
リポジトリ

n8n-security-testing について

n8n ワークフローのセキュリティを監査するためのテストスキルです。認証情報、Webhook、式、実行データの確認に使えるチェックリストとテスト方法をまとめています。

主な機能

  1. 01ワークフロー内の認証情報スキャン
  2. 02暗号化と認証情報ローテーションの確認
  3. 03Webhook 認証と入力検証
  4. 04式の脆弱性チェック
  5. 05実行ログとエラーの漏えい確認
  6. 06セキュリティ監査レポートのテンプレート

活用シーン

  1. 01エクスポートしたワークフローの秘密情報確認
  2. 02Webhook 認証とペイロードのテスト
  3. 03ログやエラー内の機密データ確認
  4. 04n8n セキュリティ監査レポートの作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/proffesor-for-testing/agentic-qe --skill n8n-security-testing

インストール後、Claude Code で/n8n-security-testingを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
n8n-security-testing.yaml
schemas/
output.json
scripts/
validate-config.json

詳細

n8n セキュリティテスト

<default_to_action> n8n のセキュリティテストを行う際は、次の手順に従ってください。

  1. ワークフロー内の認証情報の漏えいをスキャンする
  2. 機密データが暗号化されていることを確認する
  3. OAuth トークンの処理をテストする
  4. 安全でないデータ送信がないか確認する
  5. 入力のサニタイズを検証する

セキュリティのクイックチェックリスト:

  • ワークフローの JSON に認証情報が含まれていない
  • 実行ログに認証情報が含まれていない
  • OAuth トークンが適切に暗号化されている
  • API キーがバージョン管理に含まれていない
  • Webhook 認証が有効になっている
  • 入力データがサニタイズされている

重要な成功要因:

  • すべてのワークフローのエクスポートをスキャンする
  • 認証情報のローテーションをテストする
  • 保存データの暗号化を確認する
  • 監査ログを確認する </default_to_action>

クイックリファレンスカード

セキュリティリスク領域
領域リスクレベルテストの重点
認証情報の保存重大暗号化、漏えい
Webhook のセキュリティ高認証、検証
式インジェクション高入力のサニタイズ
データ漏えい中ログ、エラーメッセージ
OAuth フロー中トークンの処理、更新
認証情報の種類
種類漏えいリスクローテーション
API キー漏えいした場合は高手動
OAuth トークン中(有効期間が短い)自動
パスワード重大手動
Webhook中新しく生成

認証情報のセキュリティテスト

漏えいした認証情報をスキャンする
// Scan workflow JSON for credential exposure
async function scanForExposedCredentials(workflowId: string): Promise<CredentialScanResult> {
  const workflow = await getWorkflow(workflowId);
  const workflowJson = JSON.stringify(workflow, null, 2);

  const sensitivePatterns = [
    // API Keys
    { name: 'Generic API Key', pattern: /api[_-]?key["\s:=]+["']?([a-zA-Z0-9_-]{20,})["']?/gi },
    { name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/g },
    { name: 'AWS Secret Key', pattern: /[a-zA-Z0-9/+=]{40}/g },
    // Tokens
    { name: 'Bearer Token', pattern: /bearer\s+[a-zA-Z0-9_-]{20,}/gi },
    { name: 'JWT Token', pattern: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g },
    { name: 'Slack Token', pattern: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}/g },
    // Passwords
    { name: 'Password Field', pattern: /"password":\s*"[^"]+"/gi },
    { name: 'Secret Field', pattern: /"secret":\s*"[^"]+"/gi },
    // OAuth
    { name: 'Client Secret', pattern: /client[_-]?secret["\s:=]+["']?([a-zA-Z0-9_-]{20,})["']?/gi },
    { name: 'Refresh Token', pattern: /refresh[_-]?token["\s:=]+["']?([a-zA-Z0-9_-]{20,})["']?/gi }
  ];

  const findings: CredentialFinding[] = [];

  for (const pattern of sensitivePatterns) {
    const matches = workflowJson.match(pattern.pattern);
    if (matches) {
      for (const match of matches) {
        findings.push({
          type: pattern.name,
          location: findLocationInWorkflow(workflow, match),
          severity: 'CRITICAL',
          recommendation: `Remove ${pattern.name} from workflow. Use n8n credentials instead.`
        });
      }
    }
  }

  return {
    workflowId,
    scanned: true,
    findingsCount: findings.length,
    findings,
    secure: findings.length === 0
  };
}
認証情報の暗号化を確認する
// Verify credentials are encrypted at rest
async function verifyCredentialEncryption(credentialId: string): Promise<EncryptionResult> {
  // Get credential metadata (not the actual credential)
  const credential = await getCredentialMetadata(credentialId);

  // Check if credential data is encrypted
  const encryptionChecks = {
    // Check if stored data looks encrypted (not plain text)
    isEncrypted: !isPlainText(credential.data),
    // Check encryption algorithm
    algorithm: credential.encryptionAlgorithm || 'unknown',
    // Check key derivation
    keyDerivation: credential.keyDerivation || 'unknown',
    // Check if using instance encryption key
    instanceEncryption: credential.useInstanceKey || false
  };

  return {
    credentialId,
    credentialName: credential.name,
    credentialType: credential.type,
    encryption: encryptionChecks,
    secure: encryptionChecks.isEncrypted && encryptionChecks.algorithm !== 'unknown',
    recommendations: generateEncryptionRecommendations(encryptionChecks)
  };
}

// Check if data appears to be plain text
function isPlainText(data: string): boolean {
  // Plain text credentials often have recognizable patterns
  const plainTextPatterns = [
    /^[a-zA-Z0-9_-]+$/, // Simple alphanumeric
    /^sk-[a-zA-Z0-9]+$/, // API key format
    /^Bearer\s/, // Bearer token
  ];

  return plainTextPatterns.some(p => p.test(data));
}
認証情報のローテーションをテストする
// Test credential rotation process
async function testCredentialRotation(credentialId: string): Promise<RotationTestResult> {
  const credential = await getCredentialMetadata(credentialId);

  const rotationTests = {
    // Check if credential has rotation metadata
    hasRotationSchedule: !!credential.rotationSchedule,
    lastRotated: credential.lastRotatedAt,
    rotationDue: isRotationDue(credential),

    // Test OAuth token refresh
    oauthRefresh: credential.type.includes('oauth')
      ? await testOAuthRefresh(credentialId)
      : null,

    // Check credential age
    credentialAge: calculateAge(credential.createdAt),
    isStale: calculateAge(credential.createdAt) > 90 // 90 days
  };

  return {
    credentialId,
    rotationTests,
    recommendations: generateRotationRecommendations(rotationTests)
  };
}

// Test OAuth token refresh
async function testOAuthRefresh(credentialId: string): Promise<OAuthRefreshResult> {
  try {
    // Trigger refresh
    const refreshed = await refreshCredential(credentialId);

    return {
      success: true,
      newExpiry: refreshed.expiresAt,
      refreshedAt: new Date()
    };
  } catch (error) {
    return {
      success: false,
      error: error.message,
      recommendation: 'Re-authorize OAuth connection'
    };
  }
}

Webhook のセキュリティテスト

認証テスト
// Test webhook authentication enforcement
async function testWebhookAuthentication(webhookUrl: string): Promise<WebhookAuthResult> {
  const authTests = [
    // No authentication
    {
      name: 'No Auth',
      headers: {},
      expectedStatus: 401
    },
    // Invalid Basic Auth
    {
      name: 'Invalid Basic Auth',
      headers: { 'Authorization': 'Basic aW52YWxpZDppbnZhbGlk' },
      expectedStatus: 401
    },
    // Invalid Bearer Token
    {
      name: 'Invalid Bearer',
      headers: { 'Authorization': 'Bearer invalid-token-12345' },
      expectedStatus: 401
    },
    // Invalid Header Auth
    {
      name: 'Invalid Header Auth',
      headers: { 'X-API-Key': 'invalid-key' },
      expectedStatus: 401
    }
  ];

  const results: AuthTestResult[] = [];

  for (const test of authTests) {
    const response = await fetch(webhookUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...test.headers
      },
      body: '{}'
    });

    results.push({
      test: test.name,
      status: response.status,
      passed: response.status === test.expectedStatus,
      actualStatus: response.status,
      expectedStatus: test.expectedStatus
    });
  }

  // Check if webhook has ANY auth
  const noAuthResponse = results.find(r => r.test === 'No Auth');
  const webhookHasAuth = noAuthResponse?.status === 401;

  return {
    webhookUrl,
    hasAuthentication: webhookHasAuth,
    testResults: results,
    allTestsPassed: results.every(r => r.passed),
    recommendation: !webhookHasAuth
      ? 'CRITICAL: Enable authentication on webhook'
      : null
  };
}
入力検証テスト
// Test webhook input validation
async function testWebhookInputValidation(webhookUrl: string): Promise<InputValidationResult> {
  const maliciousPayloads = [
    // XSS attempts
    {
      name: 'XSS Script Tag',
      payload: { text: '<script>alert("xss")</script>' },
      check: 'sanitized'
    },
    {
      name: 'XSS Event Handler',
      payload: { text: '<img onerror="alert(1)" src="x">' },
      check: 'sanitized'
    },
    // SQL Injection
    {
      name: 'SQL Injection',
      payload: { id: "1; DROP TABLE users; --" },
      check: 'escaped'
    },
    // Command Injection
    {
      name: 'Command Injection',
      payload: { filename: '; rm -rf /' },
      check: 'rejected'
    },
    // Path Traversal
    {
      name: 'Path Traversal',
      payload: { path: '../../../etc/passwd' },
      check: 'rejected'
    },
    // JSON Injection
    {
      name: 'JSON Injection',
      payload: { data: '{"admin": true}' },
      check: 'escaped'
    },
    // Oversized payload
    {
      name: 'Oversized Payload',
      payload: { data: 'x'.repeat(10000000) }, // 10MB
      check: 'rejected'
    }
  ];

  const results: ValidationTestResult[] = [];

  for (const test of maliciousPayloads) {
    try {
      const response = await fetch(webhookUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(test.payload)
      });

      const responseBody = await response.text();

      results.push({
        test: test.name,
        status: response.status,
        handled: response.status !== 500, // Not a server error
        sanitized: !responseBody.includes(test.payload.text || test.payload.data),
        recommendation: response.status === 500
          ? `Input not handled safely: ${test.name}`
          : null
      });
    } catch (error) {
      results.push({
        test: test.name,
        handled: false,
        error: error.message
      });
    }
  }

  return {
    webhookUrl,
    testsRun: maliciousPayloads.length,
    passed: results.filter(r => r.handled).length,
    failed: results.filter(r => !r.handled).length,
    results,
    secure: results.every(r => r.handled)
  };
}

式のセキュリティテスト

危険な式を検出する
// Scan expressions for security vulnerabilities
async function scanExpressionsForSecurity(workflowId: string): Promise<ExpressionSecurityResult> {
  const workflow = await getWorkflow(workflowId);
  const expressions = extractExpressions(workflow);

  const dangerousPatterns = [
    // Code execution
    { name: 'eval()', pattern: /eval\s*\(/g, severity: 'CRITICAL' },
    { name: 'Function()', pattern: /new\s+Function\s*\(/g, severity: 'CRITICAL' },
    { name: 'setTimeout string', pattern: /setTimeout\s*\(\s*["'`]/g, severity: 'HIGH' },
    { name: 'setInterval string', pattern: /setInterval\s*\(\s*["'`]/g, severity: 'HIGH' },

    // File system access
    { name: 'require()', pattern: /require\s*\(/g, severity: 'HIGH' },
    { name: 'import()', pattern: /import\s*\(/g, severity: 'HIGH' },
    { name: 'fs access', pattern: /\bfs\./g, severity: 'HIGH' },

    // Process/child execution
    { name: 'child_process', pattern: /child_process/g, severity: 'CRITICAL' },
    { name: 'process.', pattern: /process\./g, severity: 'MEDIUM' },
    { name: 'exec()', pattern: /exec\s*\(/g, severity: 'CRITICAL' },
    { name: 'spawn()', pattern: /spawn\s*\(/g, severity: 'CRITICAL' },

    // Network access
    { name: 'fetch()', pattern: /fetch\s*\(/g, severity: 'MEDIUM' },
    { name: 'XMLHttpRequest', pattern: /XMLHttpRequest/g, severity: 'MEDIUM' },

    // Prototype pollution
    { name: '__proto__', pattern: /__proto__/g, severity: 'HIGH' },
    { name: 'constructor.prototype', pattern: /constructor\.prototype/g, severity: 'HIGH' }
  ];

  const findings: SecurityFinding[] = [];

  for (const expr of expressions) {
    for (const pattern of dangerousPatterns) {
      if (pattern.pattern.test(expr.expression)) {
        findings.push({
          node: expr.nodeName,
          parameter: expr.parameter,
          expression: expr.expression,
          pattern: pattern.name,
          severity: pattern.severity,
          recommendation: `Remove ${pattern.name} from expression. Use safer alternatives.`
        });
      }
    }
  }

  return {
    workflowId,
    expressionsScanned: expressions.length,
    findings,
    secure: findings.length === 0,
    criticalIssues: findings.filter(f => f.severity === 'CRITICAL').length,
    highIssues: findings.filter(f => f.severity === 'HIGH').length
  };
}

データ漏えいテスト

実行ログをスキャンする
// Scan execution logs for credential leakage
async function scanExecutionLogs(workflowId: string, executionCount: number = 10): Promise<LogScanResult> {
  const executions = await getRecentExecutions(workflowId, executionCount);
  const findings: LogFinding[] = [];

  const sensitivePatterns = [
    { name: 'Password', pattern: /password["\s:=]+["']?[^"'\s]+["']?/gi },
    { name: 'API Key', pattern: /api[_-]?key["\s:=]+["']?[^"'\s]{20,}["']?/gi },
    { name: 'Token', pattern: /token["\s:=]+["']?[a-zA-Z0-9_-]{20,}["']?/gi },
    { name: 'Secret', pattern: /secret["\s:=]+["']?[^"'\s]+["']?/gi },
    { name: 'Authorization Header', pattern: /authorization["\s:]+["']?(bearer|basic)\s+[^"'\s]+["']?/gi }
  ];

  for (const execution of executions) {
    const logString = JSON.stringify(execution.data, null, 2);

    for (const pattern of sensitivePatterns) {
      const matches = logString.match(pattern.pattern);
      if (matches) {
        findings.push({
          executionId: execution.id,
          type: pattern.name,
          matchCount: matches.length,
          severity: 'HIGH',
          recommendation: `Mask ${pattern.name} in logs`
        });
      }
    }
  }

  return {
    workflowId,
    executionsScanned: executions.length,
    findings,
    secure: findings.length === 0,
    recommendation: findings.length > 0
      ? 'Enable credential masking in n8n settings'
      : null
  };
}
エラーメッセージの露出を確認する
// Check if error messages expose sensitive information
async function checkErrorMessageSecurity(workflowId: string): Promise<ErrorMessageResult> {
  // Trigger intentional errors
  const errorScenarios = [
    { name: 'Invalid credentials', inject: { credentials: null } },
    { name: 'Invalid endpoint', inject: { url: 'https://invalid' } },
    { name: 'Database error', inject: { query: 'INVALID SQL' } }
  ];

  const findings: ErrorFinding[] = [];

  for (const scenario of errorScenarios) {
    try {
      await executeWithError(workflowId, scenario.inject);
    } catch (error) {
      const errorMessage = error.message;

      // Check for sensitive data in error
      const sensitiveData = [
        { name: 'Connection string', pattern: /mongodb:\/\/[^@]+@/i },
        { name: 'Password in URL', pattern: /:\/\/[^:]+:[^@]+@/i },
        { name: 'Full file path', pattern: /\/(?:home|Users|var)\/[^\s]+/i },
        { name: 'Stack trace', pattern: /at\s+\w+\s+\([^)]+\)/i },
        { name: 'Internal IP', pattern: /\b(?:10|172\.(?:1[6-9]|2[0-9]|3[01])|192\.168)\.\d+\.\d+\b/i }
      ];

      for (const check of sensitiveData) {
        if (check.pattern.test(errorMessage)) {
          findings.push({
            scenario: scenario.name,
            exposedData: check.name,
            severity: 'MEDIUM',
            recommendation: `Sanitize ${check.name} from error messages`
          });
        }
      }
    }
  }

  return {
    workflowId,
    scenariosTested: errorScenarios.length,
    findings,
    secure: findings.length === 0
  };
}

セキュリティレポートのテンプレート

# n8n Security Audit Report

## Summary
| Category | Status | Findings |
|----------|--------|----------|
| Credential Security | PASS/FAIL | X issues |
| Webhook Security | PASS/FAIL | X issues |
| Expression Security | PASS/FAIL | X issues |
| Data Leakage | PASS/FAIL | X issues |

## Critical Findings

### CRIT-001: API Key Exposed in Workflow
- **Location:** HTTP Request node, URL parameter
- **Impact:** Credential theft, unauthorized access
- **Fix:** Move to n8n credentials store

### CRIT-002: eval() in Expression
- **Location:** Set node, custom field
- **Impact:** Remote code execution
- **Fix:** Remove eval, use explicit logic

## Recommendations

1. **Enable webhook authentication** - All public webhooks
2. **Rotate exposed credentials** - Immediately
3. **Enable log masking** - For all credentials
4. **Regular security scans** - Weekly automated scans

## Compliance Status
- OWASP Top 10: X/10 addressed
- SOC 2: Partially compliant
- GDPR: Review data handling

関連スキル


覚えておくこと

n8n は 400 以上の連携で機密性の高い認証情報を扱います。 セキュリティテストでは、次のことが必要です。

  • 認証情報の漏えいスキャン
  • 暗号化の検証
  • Webhook 認証テスト
  • 式のセキュリティ分析
  • データ漏えいの検出

重要な実践事項: ワークフローの JSON で認証情報を決して露出させないでください。Webhook 認証を有効にしてください。ログ内の機密データをマスクしてください。認証情報を定期的にローテーションしてください。式に危険な関数が含まれていないかスキャンしてください。

コメント