| inclusion | auto |
| name | qe-n8n-security-testing |
| description | Credential exposure detection, OAuth flow validation, API key management testing, and data sanitization verification for n8n workflows. Use when validating n8n workflow security. |
| tags | ["n8n","security","credentials","oauth","api-keys","encryption","testing"] |
n8n Security Testing
<default_to_action>
When testing n8n security:
- SCAN for credential exposure in workflows
- VERIFY encryption of sensitive data
- TEST OAuth token handling
- CHECK for insecure data transmission
- 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
async function scanForExposedCredentials(workflowId: string): Promise<CredentialScanResult> {
const workflow = await getWorkflow(workflowId);
const workflowJson = JSON.stringify(workflow, null, 2);
const sensitivePatterns = [
{ 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 },
{ 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 },
{ name: 'Password Field', pattern: /"password":\s*"[^"]+"/gi },
{ : , : },
{ : , : },
{ : , : }
];
: [] = [];
( pattern sensitivePatterns) {
matches = workflowJson.(pattern.);
(matches) {
( match matches) {
findings.({
: pattern.,
: (workflow, match),
: ,
:
});
}
}
}
{
workflowId,
: ,
: findings.,
findings,
: findings. ===
};
}
Verify Credential Encryption
async function verifyCredentialEncryption(credentialId: string): Promise<EncryptionResult> {
const credential = await getCredentialMetadata(credentialId);
const encryptionChecks = {
isEncrypted: !isPlainText(credential.data),
algorithm: credential.encryptionAlgorithm || 'unknown',
keyDerivation: credential.keyDerivation || 'unknown',
instanceEncryption: credential.useInstanceKey || false
};
return {
credentialId,
credentialName: credential.name,
credentialType: credential.type,
encryption: encryptionChecks,
secure: encryptionChecks.isEncrypted && encryptionChecks.algorithm !== 'unknown',
recommendations: generateEncryptionRecommendations(encryptionChecks)
};
}
(): {
plainTextPatterns = [
,
,
,
];
plainTextPatterns.( p.(data));
}
Test Credential Rotation
async function testCredentialRotation(credentialId: string): Promise<RotationTestResult> {
const credential = await getCredentialMetadata(credentialId);
const rotationTests = {
hasRotationSchedule: !!credential.rotationSchedule,
lastRotated: credential.lastRotatedAt,
rotationDue: isRotationDue(credential),
oauthRefresh: credential.type.includes('oauth')
? await testOAuthRefresh(credentialId)
: null,
credentialAge: calculateAge(credential.createdAt),
isStale: calculateAge(credential.createdAt) > 90
};
return {
credentialId,
rotationTests,
recommendations: generateRotationRecommendations(rotationTests)
};
}
async function testOAuthRefresh(credentialId: ): <> {
{
refreshed = (credentialId);
{
: ,
: refreshed.,
: ()
};
} (error) {
{
: ,
: error.,
:
};
}
}
Webhook Security Testing
Authentication Testing
async function testWebhookAuthentication(webhookUrl: string): Promise<WebhookAuthResult> {
const authTests = [
{
name: 'No Auth',
headers: {},
expectedStatus: 401
},
{
name: 'Invalid Basic Auth',
headers: { 'Authorization': 'Basic aW52YWxpZDppbnZhbGlk' },
expectedStatus: 401
},
{
name: 'Invalid Bearer',
headers: { 'Authorization': 'Bearer invalid-token-12345' },
expectedStatus: 401
},
{
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, {
: ,
: {
: ,
...test.
},
:
});
results.({
: test.,
: response.,
: response. === test.,
: response.,
: test.
});
}
noAuthResponse = results.( r. === );
webhookHasAuth = noAuthResponse?. === ;
{
webhookUrl,
: webhookHasAuth,
: results,
: results.( r.),
: !webhookHasAuth
?
:
};
}
Input Validation Testing
async function testWebhookInputValidation(webhookUrl: string): Promise<InputValidationResult> {
const maliciousPayloads = [
{
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'
},
{
name: 'SQL Injection',
payload: { id: "1; DROP TABLE users; --" },
check: 'escaped'
},
{
name: 'Command Injection',
payload: { filename: '; rm -rf /' },
check: 'rejected'
},
{
name: 'Path Traversal',
payload: { path: '../../../etc/passwd' },
check: 'rejected'
},
{
: ,
: { : },
:
},
{
: ,
: { : .() },
:
}
];
: [] = [];
( test maliciousPayloads) {
{
response = (webhookUrl, {
: ,
: { : },
: .(test.)
});
responseBody = response.();
results.({
: test.,
: response.,
: response. !== ,
: !responseBody.(test.. || test..),
: response. ===
?
:
});
} (error) {
results.({
: test.,
: ,
: error.
});
}
}
{
webhookUrl,
: maliciousPayloads.,
: results.( r.).,
: results.( !r.).,
results,
: results.( r.)
};
}
Expression Security Testing
Detect Dangerous Expressions
async function scanExpressionsForSecurity(workflowId: string): Promise<ExpressionSecurityResult> {
const workflow = await getWorkflow(workflowId);
const expressions = extractExpressions(workflow);
const dangerousPatterns = [
{ 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' },
{ name: 'require()', pattern: /require\s*\(/g, severity: 'HIGH' },
{ name: 'import()', pattern: /import\s*\(/g, : },
{ : , : , : },
{ : , : , : },
{ : , : , : },
{ : , : , : },
{ : , : , : },
{ : , : , : },
{ : , : , : },
{ : , : , : },
{ : , : , : }
];
: [] = [];
( expr expressions) {
( pattern dangerousPatterns) {
(pattern..(expr.)) {
findings.({
: expr.,
: expr.,
: expr.,
: pattern.,
: pattern.,
:
});
}
}
}
{
workflowId,
: expressions.,
findings,
: findings. === ,
: findings.( f. === ).,
: findings.( f. === ).
};
}
Data Leakage Testing
Scan Execution Logs
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 ( pattern sensitivePatterns) {
matches = logString.(pattern.);
(matches) {
findings.({
: execution.,
: pattern.,
: matches.,
: ,
:
});
}
}
}
{
workflowId,
: executions.,
findings,
: findings. === ,
: findings. >
?
:
};
}
Check Error Message Exposure
async function checkErrorMessageSecurity(workflowId: string): Promise<ErrorMessageResult> {
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;
const sensitiveData = [
{ name: 'Connection string', pattern: /mongodb:\/\/[^@]+@/i },
{ name: 'Password in URL', pattern: /:\/\/[^:]+:[^@]+@/i },
{ : , : },
{ : , : },
{ : , : }
];
( check sensitiveData) {
(check..(errorMessage)) {
findings.({
: scenario.,
: check.,
: ,
:
});
}
}
}
}
{
workflowId,
: errorScenarios.,
findings,
: findings. ===
};
}
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
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.