
N8n Security Testing
- 150 installs
- 433 repo stars
- Updated August 4, 2026
- proffesor-for-testing/agentic-qe
Security-test n8n automations for credential leaks, unsafe HTTP nodes, SSRF, and privilege escalation before promoting agentic-qe workflows to production.
About
n8n-security-testing from proffesor-for-testing/agentic-qe guides security-focused validation of n8n workflows, probing credentials, webhooks, HTTP nodes, and graph permissions before production promotion.
- Threat models for n8n workflow graphs
- Checks secrets and outbound HTTP nodes
- Covers SSRF and auth bypass paths
- Aligns with agentic-qe security gates
- Pre-release hardening for automations
N8n Security Testing by the numbers
- 150 all-time installs (skills.sh)
- +3 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #889 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/proffesor-for-testing/agentic-qe --skill n8n-security-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 150 |
|---|---|
| repo stars | ★ 433 |
| Last updated | August 4, 2026 |
| Repository | proffesor-for-testing/agentic-qe ↗ |
What it does
Security-test n8n automations for credential leaks, unsafe HTTP nodes, SSRF, and privilege escalation before promoting agentic-qe workflows to production.
Files
n8n Security Testing
<default_to_action> When testing n8n security: 1. SCAN for credential exposure in workflows 2. VERIFY encryption of sensitive data 3. TEST OAuth token handling 4. CHECK for insecure data transmission 5. VALIDATE input sanitization
Quick Security Checklist:
- No credentials in workflow JSON
- No credentials in execution logs
- OAuth tokens properly encrypted
- API keys not in version control
- Webhook authentication enabled
- Input data sanitized
Critical Success Factors:
- Scan all workflow exports
- Test credential rotation
- Verify encryption at rest
- Check audit logging
</default_to_action>
Quick Reference Card
Security Risk Areas
| Area | Risk Level | Testing Focus |
|---|---|---|
| Credential Storage | Critical | Encryption, exposure |
| Webhook Security | High | Authentication, validation |
| Expression Injection | High | Input sanitization |
| Data Leakage | Medium | Logging, error messages |
| OAuth Flows | Medium | Token handling, refresh |
Credential Types
| Type | Exposure Risk | Rotation |
|---|---|---|
| API Keys | High if exposed | Manual |
| OAuth Tokens | Medium (short-lived) | Automatic |
| Passwords | Critical | Manual |
| Webhooks | Medium | Generate new |
---
Credential Security Testing
Scan for Exposed Credentials
// 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 Credential Encryption
// 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
// 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 Security Testing
Authentication Testing
// 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
};
}Input Validation Testing
// 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)
};
}---
Expression Security Testing
Detect Dangerous Expressions
// 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
};
}---
Data Leakage Testing
Scan Execution Logs
// 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 Error Message Exposure
// 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
};
}---
Security Report Template
# 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---
Related Skills
- n8n-workflow-testing-fundamentals
- n8n-integration-testing-patterns
- compliance-testing
---
Remember
n8n handles sensitive credentials for 400+ integrations. Security testing requires:
- Credential exposure scanning
- Encryption verification
- Webhook authentication testing
- Expression security analysis
- Data leakage detection
Critical practices: Never expose credentials in workflow JSON. Enable webhook authentication. Mask sensitive data in logs. Rotate credentials regularly. Scan expressions for dangerous functions.
# =============================================================================
# AQE Skill Evaluation Test Suite: n8n Security Testing v1.0.0
# =============================================================================
#
# Comprehensive evaluation suite for n8n security testing skill.
# Tests credential exposure detection, OAuth flow validation, API key
# management, data sanitization, and sensitive data logging prevention.
#
# Schema: .claude/skills/.validation/schemas/skill-eval.schema.json
# Validator: .claude/skills/n8n-security-testing/scripts/validate-config.json
#
# Coverage:
# - Credential exposure detection
# - OAuth token validation and handling
# - API key management and rotation
# - Sensitive data sanitization
# - Encrypted credential verification
# - Execution log security
#
# =============================================================================
skill: n8n-security-testing
version: 1.0.0
description: >
Comprehensive evaluation suite for n8n security testing skill.
Tests credential exposure prevention, OAuth flow validation, API key
management, data encryption, sanitization verification, and prevention
of sensitive data logging in n8n workflows.
# =============================================================================
# Multi-Model Configuration
# =============================================================================
models_to_test:
- claude-opus-4-8 # Capability ceiling (high-stakes skill)
- claude-sonnet-4-6 # Primary (high accuracy expected)
- claude-haiku-4-5 # Fast model (minimum quality floor)
# =============================================================================
# MCP Integration Configuration
# =============================================================================
mcp_integration:
enabled: true
namespace: skill-validation
query_patterns: true
track_outcomes: true
store_patterns: true
share_learning: true
update_quality_gate: true
target_agents:
- qe-learning-coordinator
- qe-queen-coordinator
- qe-security-scanner
# =============================================================================
# ReasoningBank Learning Configuration
# =============================================================================
learning:
store_success_patterns: true
store_failure_patterns: true
pattern_ttl_days: 90
min_confidence_to_store: 0.7
cross_model_comparison: true
# =============================================================================
# Result Format Configuration
# =============================================================================
result_format:
json_output: true
markdown_report: true
include_raw_output: false
include_timing: true
include_token_usage: true
# =============================================================================
# Environment Setup
# =============================================================================
setup:
required_tools:
- jq
environment_variables:
SECURITY_SCANNING: "true"
CREDENTIAL_DETECTION: "strict"
# =============================================================================
# TEST CASES
# =============================================================================
test_cases:
# ---------------------------------------------------------------------------
# CATEGORY: Credential Exposure Detection
# ---------------------------------------------------------------------------
- id: tc001_hardcoded_api_key_detection
description: "Detect hardcoded API keys in workflow JSON"
category: credentials
priority: critical
input:
workflow_json:
nodes:
- name: "HTTP Request"
parameters:
headers:
Authorization: "Bearer sk_live_51234567890abcdef"
url: "https://api.example.com/data"
scan_mode: strict
context:
check_headers: true
check_urls: true
check_parameters: true
expected_output:
must_contain:
- "credential"
- "exposed"
- "API key"
- "Bearer"
must_not_contain:
- "safe"
severity_classification: critical
finding_count:
min: 1
validation:
schema_check: true
keyword_match_threshold: 0.85
reasoning_quality_min: 0.8
timeout_ms: 30000
- id: tc002_password_in_workflow_detection
description: "Detect plaintext passwords in workflow nodes"
category: credentials
priority: critical
input:
workflow_json:
nodes:
- name: "Database"
type: "postgres"
parameters:
host: "db.example.com"
username: "admin"
password: "mySecretPassword123"
- name: "Email Service"
parameters:
auth_type: "basic"
username: "user@example.com"
password: "password456"
context:
database_types: ["postgres", "mysql", "mongodb"]
expected_output:
must_contain:
- "password"
- "plaintext"
- "credential"
- "exposed"
must_not_contain:
- "encrypted"
severity_classification: critical
validation:
schema_check: true
keyword_match_threshold: 0.85
# ---------------------------------------------------------------------------
# CATEGORY: OAuth and Token Handling
# ---------------------------------------------------------------------------
- id: tc003_oauth_token_validation
description: "Validate proper OAuth token handling"
category: oauth
priority: critical
input:
oauth_config:
- name: "Google OAuth"
has_token_storage: true
token_encrypted: true
token_refresh_enabled: true
token_expiry_checked: true
valid: true
- name: "GitHub OAuth"
has_token_storage: true
token_encrypted: true
token_refresh_enabled: false
token_expiry_checked: true
valid: true
context:
standard: OAuth2
expected_output:
must_contain:
- "OAuth"
- "token"
- "encrypted"
- "valid"
must_not_contain:
- "plaintext token"
severity_classification: info
validation:
schema_check: true
keyword_match_threshold: 0.8
- id: tc004_insecure_token_transmission
description: "Detect insecure token transmission methods"
category: oauth
priority: critical
input:
issues:
- node: "HTTP Request"
token_location: "query_parameter"
secure: false
issue: "tokens in URL are logged and cached"
- node: "API Call"
token_location: "request_body"
secure: true
issue: "none"
- node: "Legacy Service"
token_location: "custom_header"
protocol: "http"
secure: false
issue: "HTTP not HTTPS"
context:
requirement: "HTTPS only"
expected_output:
must_contain:
- "insecure"
- "transmission"
- "token"
- "HTTP"
must_not_contain:
- "secure"
severity_classification: critical
validation:
schema_check: true
keyword_match_threshold: 0.85
# ---------------------------------------------------------------------------
# CATEGORY: Credential Storage and Encryption
# ---------------------------------------------------------------------------
- id: tc005_credential_storage_verification
description: "Verify credentials stored with encryption"
category: encryption
priority: critical
input:
credentials:
- id: "slack_webhook"
storage_type: "encrypted"
encryption_algorithm: "AES-256"
key_rotation_policy: "90_days"
status: valid
- id: "database_password"
storage_type: "plaintext"
issue: "stored in plaintext"
status: invalid
- id: "api_key"
storage_type: "encrypted"
encryption_algorithm: "AES-256"
status: valid
context:
standard: industry_best_practices
expected_output:
must_contain:
- "encryption"
- "AES-256"
- "plaintext"
must_not_contain:
- "all secure"
severity_classification: critical
validation:
schema_check: true
keyword_match_threshold: 0.85
- id: tc006_key_rotation_policy
description: "Verify API key rotation policies are in place"
category: encryption
priority: high
input:
rotation_policies:
- credential: "api_key_prod"
rotation_days: 90
last_rotated: "2025-01-20"
overdue: false
status: compliant
- credential: "slack_token"
rotation_days: 180
last_rotated: "2024-08-15"
overdue: true
status: non_compliant
context:
requirement: "rotate every 90 days"
expected_output:
must_contain:
- "rotation"
- "policy"
- "overdue"
must_not_contain:
- "no rotation"
severity_classification: high
validation:
schema_check: true
keyword_match_threshold: 0.75
# ---------------------------------------------------------------------------
# CATEGORY: Data Sanitization and Logging
# ---------------------------------------------------------------------------
- id: tc007_sensitive_data_in_logs
description: "Detect sensitive data exposure in execution logs"
category: logging
priority: critical
input:
log_samples:
- message: "Executing HTTP call to /api/data with headers: Authorization: Bearer sk_live_123"
contains_sensitive: true
issue: "API key exposed in logs"
- message: "Database query executed on user@example.com"
contains_sensitive: true
issue: "Credential exposed in logs"
- message: "Workflow execution started for batch processing"
contains_sensitive: false
issue: "none"
context:
log_level: DEBUG
expected_output:
must_contain:
- "sensitive"
- "exposed"
- "logs"
- "credential"
must_not_contain:
- "no exposure"
severity_classification: critical
finding_count:
min: 2
validation:
schema_check: true
keyword_match_threshold: 0.85
- id: tc008_data_masking_in_output
description: "Verify sensitive fields are masked in output"
category: logging
priority: critical
input:
output_nodes:
- node: "Set User Data"
fields:
- name: email
value: "user@example.com"
should_mask: false
- name: ssn
value: "123-45-6789"
should_mask: true
status: masked
- name: password_hash
value: "bcrypt_hash_..."
should_mask: true
status: not_masked
context:
requirement: "mask PII in outputs"
expected_output:
must_contain:
- "mask"
- "PII"
- "masked"
must_not_contain:
- "ssn"
- "password"
severity_classification: high
validation:
schema_check: true
keyword_match_threshold: 0.8
# ---------------------------------------------------------------------------
# CATEGORY: Secure Configuration Validation
# ---------------------------------------------------------------------------
- id: tc009_https_enforcement
description: "Verify HTTPS is enforced for external APIs"
category: configuration
priority: critical
input:
api_calls:
- url: "https://api.example.com/data"
protocol: HTTPS
secure: true
- url: "http://legacy-service.local/api"
protocol: HTTP
secure: false
environment: production
context:
requirement: "HTTPS for production"
expected_output:
must_contain:
- "HTTPS"
- "insecure"
- "HTTP"
must_not_contain:
- "all HTTPS"
severity_classification: critical
validation:
schema_check: true
keyword_match_threshold: 0.8
- id: tc010_credential_scope_validation
description: "Validate credentials have minimum necessary scope"
category: configuration
priority: high
input:
credentials:
- name: "Google API"
permissions: ["calendar.read"]
required_permissions: ["calendar.read"]
status: correct_scope
- name: "GitHub Token"
permissions: ["repo", "admin:org_hook", "admin:repo_hook"]
required_permissions: ["repo"]
status: over_scoped
- name: "Stripe API"
permissions: ["charges.read"]
required_permissions: ["charges.read", "customers.read"]
status: under_scoped
context:
principle: "least_privilege"
expected_output:
must_contain:
- "scope"
- "over-scoped"
- "permissions"
must_not_contain:
- "all correct"
severity_classification: medium
validation:
schema_check: true
keyword_match_threshold: 0.75
# =============================================================================
# SUCCESS CRITERIA
# =============================================================================
success_criteria:
pass_rate: 0.85
critical_pass_rate: 1.0
avg_reasoning_quality: 0.75
max_execution_time_ms: 300000
cross_model_variance: 0.15
# =============================================================================
# METADATA
# =============================================================================
metadata:
author: "n8n-integration-test"
created: "2026-02-02"
last_updated: "2026-02-02"
coverage_target: >
n8n security testing including credential exposure detection (hardcoded
API keys, passwords), OAuth token validation, credential encryption
verification, key rotation policies, sensitive data masking in logs,
HTTPS enforcement, and credential scope validation per least-privilege
principle. 10 test cases with 85% pass rate and 100% critical pass rate.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://agentic-qe.dev/schemas/skills/n8n-security-testing/output.json",
"title": "N8N Security Testing Skill Output Schema",
"description": "Schema for n8n-security-testing skill output. Validates credential security, data exposure, and workflow security vulnerabilities.",
"type": "object",
"required": ["skillName", "version", "timestamp", "status", "trustTier", "output"],
"properties": {
"skillName": {
"type": "string",
"const": "n8n-security-testing"
},
"version": {
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9]+)?$"
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string",
"enum": ["success", "partial", "failed", "skipped"]
},
"trustTier": {
"type": "integer",
"const": 3
},
"output": {
"type": "object",
"required": ["summary", "securityFindings", "credentialAudit"],
"properties": {
"summary": {
"type": "string",
"minLength": 20,
"maxLength": 2000
},
"securityFindings": {
"type": "array",
"items": {
"$ref": "#/$defs/securityFinding"
},
"maxItems": 500
},
"credentialAudit": {
"$ref": "#/$defs/credentialAudit"
},
"workflowValidation": {
"$ref": "#/$defs/workflowValidation"
},
"dataExposureRisks": {
"type": "array",
"items": {
"$ref": "#/$defs/dataExposureRisk"
}
},
"nodeSecurityScores": {
"type": "array",
"items": {
"$ref": "#/$defs/nodeSecurityScore"
}
},
"recommendations": {
"type": "array",
"items": {
"$ref": "#/$defs/recommendation"
},
"maxItems": 100
},
"complianceChecks": {
"type": "array",
"items": {
"$ref": "#/$defs/complianceCheck"
}
},
"metrics": {
"$ref": "#/$defs/metrics"
}
}
},
"metadata": {
"$ref": "#/$defs/metadata"
},
"validation": {
"$ref": "#/$defs/validationResult"
},
"learning": {
"$ref": "#/$defs/learningData"
}
},
"$defs": {
"securityFinding": {
"type": "object",
"required": ["id", "title", "severity", "category"],
"properties": {
"id": {
"type": "string",
"pattern": "^SEC-N8N-\\d{3,6}$"
},
"title": {
"type": "string",
"minLength": 10,
"maxLength": 200
},
"description": {
"type": "string",
"maxLength": 2000
},
"severity": {
"type": "string",
"enum": ["critical", "high", "medium", "low", "info"]
},
"category": {
"type": "string",
"enum": ["credential-exposure", "data-leakage", "injection", "auth-bypass", "insecure-config", "sensitive-data", "access-control"]
},
"nodeName": {
"type": "string"
},
"nodeType": {
"type": "string"
},
"location": {
"type": "object",
"properties": {
"workflowId": { "type": "string" },
"nodeId": { "type": "string" },
"parameter": { "type": "string" }
}
},
"evidence": {
"type": "string",
"maxLength": 5000
},
"remediation": {
"type": "string",
"maxLength": 2000
},
"cwe": {
"type": "string",
"pattern": "^CWE-\\d{1,4}$"
},
"owasp": {
"type": "string",
"pattern": "^A(0[1-9]|10):20(21|25)$"
},
"falsePositive": {
"type": "boolean"
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
},
"credentialAudit": {
"type": "object",
"required": ["totalCredentials", "secureCredentials", "insecureCredentials"],
"properties": {
"totalCredentials": { "type": "integer", "minimum": 0 },
"secureCredentials": { "type": "integer", "minimum": 0 },
"insecureCredentials": { "type": "integer", "minimum": 0 },
"expiredCredentials": { "type": "integer", "minimum": 0 },
"unusedCredentials": { "type": "integer", "minimum": 0 },
"credentialIssues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"credentialName": { "type": "string" },
"issue": { "type": "string" },
"severity": { "type": "string", "enum": ["critical", "high", "medium", "low"] }
}
}
},
"overallScore": { "type": "number", "minimum": 0, "maximum": 100 }
}
},
"workflowValidation": {
"type": "object",
"required": ["workflowId", "secure"],
"properties": {
"workflowId": { "type": "string" },
"workflowName": { "type": "string" },
"secure": { "type": "boolean" },
"criticalFindings": { "type": "integer", "minimum": 0 },
"highFindings": { "type": "integer", "minimum": 0 },
"mediumFindings": { "type": "integer", "minimum": 0 },
"lowFindings": { "type": "integer", "minimum": 0 },
"securityScore": { "type": "number", "minimum": 0, "maximum": 100 },
"grade": { "type": "string", "pattern": "^[A-F][+-]?$" },
"riskLevel": { "type": "string", "enum": ["critical", "high", "medium", "low", "minimal"] }
}
},
"dataExposureRisk": {
"type": "object",
"required": ["riskType", "severity"],
"properties": {
"riskType": {
"type": "string",
"enum": ["pii-exposure", "credential-leak", "api-key-exposure", "token-exposure", "logging-sensitive-data", "external-transmission"]
},
"severity": { "type": "string", "enum": ["critical", "high", "medium", "low"] },
"affectedNodes": { "type": "array", "items": { "type": "string" } },
"dataTypes": { "type": "array", "items": { "type": "string" } },
"description": { "type": "string" },
"mitigation": { "type": "string" }
}
},
"nodeSecurityScore": {
"type": "object",
"required": ["nodeName", "nodeType", "score"],
"properties": {
"nodeName": { "type": "string" },
"nodeType": { "type": "string" },
"score": { "type": "number", "minimum": 0, "maximum": 100 },
"grade": { "type": "string", "pattern": "^[A-F][+-]?$" },
"vulnerabilities": { "type": "integer", "minimum": 0 },
"riskFactors": { "type": "array", "items": { "type": "string" } }
}
},
"complianceCheck": {
"type": "object",
"required": ["standard", "passed"],
"properties": {
"standard": { "type": "string", "enum": ["GDPR", "HIPAA", "PCI-DSS", "SOC2", "OWASP"] },
"passed": { "type": "boolean" },
"requirements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"requirement": { "type": "string" },
"passed": { "type": "boolean" },
"notes": { "type": "string" }
}
}
}
}
},
"recommendation": {
"type": "object",
"required": ["id", "title", "priority"],
"properties": {
"id": { "type": "string", "pattern": "^REC-\\d{3,6}$" },
"title": { "type": "string", "maxLength": 200 },
"description": { "type": "string", "maxLength": 2000 },
"priority": { "type": "string", "enum": ["critical", "high", "medium", "low"] },
"relatedFindings": { "type": "array", "items": { "type": "string" } },
"effort": { "type": "string", "enum": ["trivial", "low", "medium", "high", "major"] }
}
},
"metrics": {
"type": "object",
"properties": {
"totalFindings": { "type": "integer", "minimum": 0 },
"criticalCount": { "type": "integer", "minimum": 0 },
"highCount": { "type": "integer", "minimum": 0 },
"mediumCount": { "type": "integer", "minimum": 0 },
"lowCount": { "type": "integer", "minimum": 0 },
"nodesScanned": { "type": "integer", "minimum": 0 },
"credentialsAudited": { "type": "integer", "minimum": 0 },
"securityScore": { "type": "number", "minimum": 0, "maximum": 100 }
}
},
"metadata": {
"type": "object",
"properties": {
"executionTimeMs": { "type": "integer", "minimum": 0 },
"toolsUsed": { "type": "array", "items": { "type": "string" } },
"agentId": { "type": "string", "pattern": "^qe-[a-z][a-z0-9-]*$" },
"n8nVersion": { "type": "string" },
"scanDepth": { "type": "string", "enum": ["quick", "standard", "deep"] }
}
},
"validationResult": {
"type": "object",
"properties": {
"schemaValid": { "type": "boolean" },
"contentValid": { "type": "boolean" },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 }
}
},
"learningData": {
"type": "object",
"properties": {
"patternsDetected": { "type": "array", "items": { "type": "string" } },
"reward": { "type": "number", "minimum": 0, "maximum": 1 }
}
}
}
}
{
"skillName": "n8n-security-testing",
"skillVersion": "1.0.0",
"requiredTools": [
"jq"
],
"optionalTools": [],
"schemaPath": "schemas/output.json",
"requiredFields": [
"skillName",
"status",
"output",
"output.summary",
"output.securityFindings",
"output.credentialAudit"
],
"requiredNonEmptyFields": [],
"mustContainTerms": [
"security",
"credential"
],
"mustNotContainTerms": [
"TODO",
"FIXME"
],
"enumValidations": {
".status": [
"success",
"partial",
"failed",
"skipped"
]
}
}