mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
feat: implement Phase 2 security fixes (partial - HIGH-01, HIGH-04, HIGH-08, MEDIUM-02, MEDIUM-05)
Implements 5 of 8 security fixes from Issue #265 Phase 2: ✅ COMPLETED: - **MEDIUM-05: Dependency Audit Documentation** - Added Security & Dependencies section to README.md - Documents that n8n package vulnerabilities are upstream responsibilities - Explains our direct dependencies are kept up to date - Provides security update workflow - **HIGH-01: SQL Injection ESLint Safeguards** - Installed ESLint with TypeScript support - Created eslint.config.js with no-restricted-syntax rule - Blocks template literals in db.exec() calls - Added JSDoc @security comments to 8 existing db.exec() calls - All static SQL statements documented and safe - **MEDIUM-02: Input Length Limits** - Reduced express.json() body size from 10mb to 1mb - Added URL length validation middleware (2048 char limit) - Returns HTTP 414 for oversized URLs - Logs input_validation_failure events - **HIGH-08: Security Headers** - Installed helmet package - Configured comprehensive CSP, Referrer-Policy, HSTS, Permissions-Policy - Disabled x-powered-by header - All security headers now present on responses - **HIGH-04: Error Sanitization Consistency** - Updated Express global error handler - Now uses sanitizeErrorForClient() method - Ensures no stack traces or internal details leak in any mode - Production-safe error responses ⏳ REMAINING (to be completed): - HIGH-06: CORS production validation - HIGH-05: Multi-tenant shared mode safety check - MEDIUM-04: Audit logging event field verification Files modified: - README.md (new Security & Dependencies section) - package.json, package-lock.json (eslint, helmet dependencies) - eslint.config.js (new ESLint flat config) - src/http-server-single-session.ts (security headers, input limits, error handler) - src/templates/template-repository.ts (JSDoc security comments) - src/scripts/fetch-templates.ts (JSDoc security comments) Part of Issue #265 security audit remediation. Next: Complete remaining 3 fixes, add tests, version bump to 2.16.4. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
39
README.md
39
README.md
@@ -995,6 +995,45 @@ MIT License - see [LICENSE](LICENSE) for details.
|
|||||||
- 🔗 Linking back to this repo
|
- 🔗 Linking back to this repo
|
||||||
|
|
||||||
|
|
||||||
|
## 🔒 Security & Dependencies
|
||||||
|
|
||||||
|
### Upstream Dependencies
|
||||||
|
|
||||||
|
This project depends on n8n packages which are maintained by the n8n team. Any vulnerabilities in the following packages are upstream responsibilities:
|
||||||
|
- `n8n-workflow`
|
||||||
|
- `n8n-nodes-base`
|
||||||
|
- `@n8n/n8n-nodes-langchain`
|
||||||
|
- Related n8n dependencies (@n8n/*, @langchain/*)
|
||||||
|
|
||||||
|
We sync with n8n releases using `npm run update:n8n` when new versions are published by the n8n team.
|
||||||
|
|
||||||
|
### Direct Dependencies
|
||||||
|
|
||||||
|
Our direct dependencies (express, axios, helmet, etc.) are kept up to date. Run `npm audit` to check current status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check for vulnerabilities in project dependencies
|
||||||
|
npm audit
|
||||||
|
|
||||||
|
# Note: Vulnerabilities in n8n packages are upstream responsibilities
|
||||||
|
# and will be fixed by the n8n team in their releases
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Updates
|
||||||
|
|
||||||
|
- **Monitor n8n releases**: https://github.com/n8n-io/n8n/releases
|
||||||
|
- **Check our dependencies**: `npm audit`
|
||||||
|
- **Check for n8n updates**: `npm run update:n8n:check`
|
||||||
|
- **Update n8n packages**: `npm run update:n8n` (when new version available)
|
||||||
|
|
||||||
|
### Security Configuration
|
||||||
|
|
||||||
|
For production deployments, ensure you configure:
|
||||||
|
- Strong `AUTH_TOKEN` (minimum 32 characters)
|
||||||
|
- Specific `CORS_ORIGIN` (not wildcard `*`)
|
||||||
|
- `WEBHOOK_SECURITY_MODE=strict` (default)
|
||||||
|
- Review [Security Best Practices](./docs/HTTP_DEPLOYMENT.md#security-best-practices)
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
Contributions are welcome! Please:
|
Contributions are welcome! Please:
|
||||||
|
|||||||
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import tseslint from '@typescript-eslint/eslint-plugin';
|
||||||
|
import tsparser from '@typescript-eslint/parser';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
files: ['src/**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsparser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
project: './tsconfig.json'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': tseslint
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-restricted-syntax': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
selector: 'CallExpression[callee.property.name="exec"] TemplateLiteral',
|
||||||
|
message: 'SECURITY: Template literals in db.exec() can lead to SQL injection. Use parameterized queries with db.prepare().all() instead. See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-01)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
1006
package-lock.json
generated
1006
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@
|
|||||||
"test:cleanup:orphans": "tsx tests/integration/n8n-api/scripts/cleanup-orphans.ts",
|
"test:cleanup:orphans": "tsx tests/integration/n8n-api/scripts/cleanup-orphans.ts",
|
||||||
"test:e2e": "vitest run tests/e2e",
|
"test:e2e": "vitest run tests/e2e",
|
||||||
"lint": "tsc --noEmit",
|
"lint": "tsc --noEmit",
|
||||||
|
"lint:eslint": "eslint 'src/**/*.ts'",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"update:n8n": "node scripts/update-n8n-deps.js",
|
"update:n8n": "node scripts/update-n8n-deps.js",
|
||||||
"update:n8n:check": "node scripts/update-n8n-deps.js --dry-run",
|
"update:n8n:check": "node scripts/update-n8n-deps.js --dry-run",
|
||||||
@@ -118,11 +119,14 @@
|
|||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.3",
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^22.15.30",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
||||||
|
"@typescript-eslint/parser": "^8.45.0",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/runner": "^3.2.4",
|
"@vitest/runner": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"axios-mock-adapter": "^2.1.0",
|
"axios-mock-adapter": "^2.1.0",
|
||||||
|
"eslint": "^9.37.0",
|
||||||
"fishery": "^2.3.1",
|
"fishery": "^2.3.1",
|
||||||
"msw": "^2.10.4",
|
"msw": "^2.10.4",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
@@ -137,6 +141,7 @@
|
|||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^7.1.5",
|
"express-rate-limit": "^7.1.5",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
"lru-cache": "^11.2.1",
|
"lru-cache": "^11.2.1",
|
||||||
"n8n": "^1.113.3",
|
"n8n": "^1.113.3",
|
||||||
"n8n-core": "^1.112.1",
|
"n8n-core": "^1.112.1",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
|
import helmet from 'helmet';
|
||||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||||
import { N8NDocumentationMCPServer } from './mcp/server';
|
import { N8NDocumentationMCPServer } from './mcp/server';
|
||||||
@@ -677,9 +678,11 @@ export class SingleSessionHTTPServer {
|
|||||||
*/
|
*/
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Create JSON parser middleware for endpoints that need it
|
// Create JSON parser middleware for endpoints that need it
|
||||||
const jsonParser = express.json({ limit: '10mb' });
|
// SECURITY: Limit request body size to prevent resource exhaustion
|
||||||
|
// See: https://github.com/czlonkowski/n8n-mcp/issues/265 (MEDIUM-02)
|
||||||
|
const jsonParser = express.json({ limit: '1mb' });
|
||||||
|
|
||||||
// Configure trust proxy for correct IP logging behind reverse proxies
|
// Configure trust proxy for correct IP logging behind reverse proxies
|
||||||
const trustProxy = process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) : 0;
|
const trustProxy = process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) : 0;
|
||||||
@@ -690,15 +693,60 @@ export class SingleSessionHTTPServer {
|
|||||||
|
|
||||||
// DON'T use any body parser globally - StreamableHTTPServerTransport needs raw stream
|
// DON'T use any body parser globally - StreamableHTTPServerTransport needs raw stream
|
||||||
// Only use JSON parser for specific endpoints that need it
|
// Only use JSON parser for specific endpoints that need it
|
||||||
|
|
||||||
// Security headers
|
// SECURITY: Limit URL length to prevent buffer overflow attacks
|
||||||
|
// See: https://github.com/czlonkowski/n8n-mcp/issues/265 (MEDIUM-02)
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
if (req.url.length > 2048) {
|
||||||
res.setHeader('X-Frame-Options', 'DENY');
|
logger.warn('Request rejected: URL too long', {
|
||||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
urlLength: req.url.length,
|
||||||
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
ip: req.ip,
|
||||||
|
userAgent: req.get('user-agent'),
|
||||||
|
event: 'input_validation_failure'
|
||||||
|
});
|
||||||
|
return res.status(414).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: { code: -32600, message: 'URI Too Long' },
|
||||||
|
id: null
|
||||||
|
});
|
||||||
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// SECURITY: Comprehensive security headers via helmet
|
||||||
|
// See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-08)
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
scriptSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'"],
|
||||||
|
fontSrc: ["'self'"],
|
||||||
|
objectSrc: ["'none'"],
|
||||||
|
mediaSrc: ["'self'"],
|
||||||
|
frameSrc: ["'none'"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
||||||
|
hsts: {
|
||||||
|
maxAge: 31536000,
|
||||||
|
includeSubDomains: true,
|
||||||
|
preload: true
|
||||||
|
},
|
||||||
|
permissionsPolicy: {
|
||||||
|
features: {
|
||||||
|
camera: ["'none'"],
|
||||||
|
microphone: ["'none'"],
|
||||||
|
geolocation: ["'none'"],
|
||||||
|
payment: ["'none'"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Remove X-Powered-By header
|
||||||
|
app.disable('x-powered-by');
|
||||||
|
|
||||||
// CORS configuration
|
// CORS configuration
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
@@ -1213,17 +1261,19 @@ export class SingleSessionHTTPServer {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Error handler
|
// SECURITY: Error handler with sanitization
|
||||||
|
// See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-04)
|
||||||
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.error('Express error handler:', err);
|
logger.error('Express error handler:', err);
|
||||||
|
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.status(500).json({
|
// Use sanitizeErrorForClient() to ensure no stack traces or internal details leak
|
||||||
|
const sanitized = this.sanitizeErrorForClient(err);
|
||||||
|
res.status(500).json({
|
||||||
jsonrpc: '2.0',
|
jsonrpc: '2.0',
|
||||||
error: {
|
error: {
|
||||||
code: -32603,
|
code: -32603,
|
||||||
message: 'Internal server error',
|
message: sanitized.message
|
||||||
data: process.env.NODE_ENV === 'development' ? err.message : undefined
|
|
||||||
},
|
},
|
||||||
id: null
|
id: null
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,6 +116,12 @@ function insertAndRankConfigs(db: any, configs: any[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rank configs per node_type by template popularity
|
// Rank configs per node_type by template popularity
|
||||||
|
/**
|
||||||
|
* @security SQL: Static UPDATE statement with no user input.
|
||||||
|
* Template literal used for multi-line formatting only.
|
||||||
|
* See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-01)
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
db.exec(`
|
db.exec(`
|
||||||
UPDATE template_node_configs
|
UPDATE template_node_configs
|
||||||
SET rank = (
|
SET rank = (
|
||||||
@@ -127,6 +133,11 @@ function insertAndRankConfigs(db: any, configs: any[]) {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
// Keep only top 10 per node_type
|
// Keep only top 10 per node_type
|
||||||
|
/**
|
||||||
|
* @security SQL: Static DELETE statement with no user input.
|
||||||
|
* Template literal used for multi-line formatting only.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
db.exec(`
|
db.exec(`
|
||||||
DELETE FROM template_node_configs
|
DELETE FROM template_node_configs
|
||||||
WHERE id NOT IN (
|
WHERE id NOT IN (
|
||||||
@@ -294,6 +305,12 @@ async function fetchTemplates(
|
|||||||
|
|
||||||
if (!hasMetadataColumn) {
|
if (!hasMetadataColumn) {
|
||||||
console.log('📋 Adding metadata columns to existing schema...');
|
console.log('📋 Adding metadata columns to existing schema...');
|
||||||
|
/**
|
||||||
|
* @security SQL: Static ALTER TABLE statement with no user input.
|
||||||
|
* Template literal used for multi-line formatting only.
|
||||||
|
* See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-01)
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
db.exec(`
|
db.exec(`
|
||||||
ALTER TABLE templates ADD COLUMN metadata_json TEXT;
|
ALTER TABLE templates ADD COLUMN metadata_json TEXT;
|
||||||
ALTER TABLE templates ADD COLUMN metadata_generated_at DATETIME;
|
ALTER TABLE templates ADD COLUMN metadata_generated_at DATETIME;
|
||||||
|
|||||||
@@ -64,33 +64,54 @@ export class TemplateRepository {
|
|||||||
} else {
|
} else {
|
||||||
// Create FTS5 virtual table
|
// Create FTS5 virtual table
|
||||||
logger.info('Creating FTS5 virtual table for templates...');
|
logger.info('Creating FTS5 virtual table for templates...');
|
||||||
|
/**
|
||||||
|
* @security SQL: Static CREATE TABLE statement with no user input.
|
||||||
|
* Template literal used for multi-line formatting only.
|
||||||
|
* See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-01)
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
|
CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
|
||||||
name, description, content=templates
|
name, description, content=templates
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Create triggers to keep FTS5 in sync
|
// Create triggers to keep FTS5 in sync
|
||||||
|
/**
|
||||||
|
* @security SQL: Static CREATE TRIGGER statement with no user input.
|
||||||
|
* Template literal used for multi-line formatting only.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
CREATE TRIGGER IF NOT EXISTS templates_ai AFTER INSERT ON templates BEGIN
|
CREATE TRIGGER IF NOT EXISTS templates_ai AFTER INSERT ON templates BEGIN
|
||||||
INSERT INTO templates_fts(rowid, name, description)
|
INSERT INTO templates_fts(rowid, name, description)
|
||||||
VALUES (new.id, new.name, new.description);
|
VALUES (new.id, new.name, new.description);
|
||||||
END;
|
END;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @security SQL: Static CREATE TRIGGER statement with no user input.
|
||||||
|
* Template literal used for multi-line formatting only.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
CREATE TRIGGER IF NOT EXISTS templates_au AFTER UPDATE ON templates BEGIN
|
CREATE TRIGGER IF NOT EXISTS templates_au AFTER UPDATE ON templates BEGIN
|
||||||
UPDATE templates_fts SET name = new.name, description = new.description
|
UPDATE templates_fts SET name = new.name, description = new.description
|
||||||
WHERE rowid = new.id;
|
WHERE rowid = new.id;
|
||||||
END;
|
END;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @security SQL: Static CREATE TRIGGER statement with no user input.
|
||||||
|
* Template literal used for multi-line formatting only.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
CREATE TRIGGER IF NOT EXISTS templates_ad AFTER DELETE ON templates BEGIN
|
CREATE TRIGGER IF NOT EXISTS templates_ad AFTER DELETE ON templates BEGIN
|
||||||
DELETE FROM templates_fts WHERE rowid = old.id;
|
DELETE FROM templates_fts WHERE rowid = old.id;
|
||||||
END;
|
END;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
logger.info('FTS5 support enabled for template search');
|
logger.info('FTS5 support enabled for template search');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -524,8 +545,14 @@ export class TemplateRepository {
|
|||||||
try {
|
try {
|
||||||
// Clear existing FTS data
|
// Clear existing FTS data
|
||||||
this.db.exec('DELETE FROM templates_fts');
|
this.db.exec('DELETE FROM templates_fts');
|
||||||
|
|
||||||
// Repopulate from templates table
|
// Repopulate from templates table
|
||||||
|
/**
|
||||||
|
* @security SQL: Static INSERT-SELECT statement with no user input.
|
||||||
|
* Template literal used for multi-line formatting only.
|
||||||
|
* See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-01)
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
INSERT INTO templates_fts(rowid, name, description)
|
INSERT INTO templates_fts(rowid, name, description)
|
||||||
SELECT id, name, description FROM templates
|
SELECT id, name, description FROM templates
|
||||||
|
|||||||
Reference in New Issue
Block a user