mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 22:42:04 +00:00
Compare commits
1 Commits
v2.33.1
...
security/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
217825c6e1 |
39
README.md
39
README.md
@@ -995,6 +995,45 @@ MIT License - see [LICENSE](LICENSE) for details.
|
||||
- 🔗 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
|
||||
|
||||
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:e2e": "vitest run tests/e2e",
|
||||
"lint": "tsc --noEmit",
|
||||
"lint:eslint": "eslint 'src/**/*.ts'",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"update:n8n": "node scripts/update-n8n-deps.js",
|
||||
"update:n8n:check": "node scripts/update-n8n-deps.js --dry-run",
|
||||
@@ -118,11 +119,14 @@
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^22.15.30",
|
||||
"@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/runner": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"axios": "^1.11.0",
|
||||
"axios-mock-adapter": "^2.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"fishery": "^2.3.1",
|
||||
"msw": "^2.10.4",
|
||||
"nodemon": "^3.1.10",
|
||||
@@ -137,6 +141,7 @@
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^8.1.0",
|
||||
"lru-cache": "^11.2.1",
|
||||
"n8n": "^1.113.3",
|
||||
"n8n-core": "^1.112.1",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import express from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import helmet from 'helmet';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
import { N8NDocumentationMCPServer } from './mcp/server';
|
||||
@@ -677,9 +678,11 @@ export class SingleSessionHTTPServer {
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
const app = express();
|
||||
|
||||
|
||||
// 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
|
||||
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
|
||||
// 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) => {
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
if (req.url.length > 2048) {
|
||||
logger.warn('Request rejected: URL too long', {
|
||||
urlLength: req.url.length,
|
||||
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();
|
||||
});
|
||||
|
||||
// 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
|
||||
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) => {
|
||||
logger.error('Express error handler:', err);
|
||||
|
||||
|
||||
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',
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Internal server error',
|
||||
data: process.env.NODE_ENV === 'development' ? err.message : undefined
|
||||
message: sanitized.message
|
||||
},
|
||||
id: null
|
||||
});
|
||||
|
||||
@@ -116,6 +116,12 @@ function insertAndRankConfigs(db: any, configs: any[]) {
|
||||
}
|
||||
|
||||
// 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(`
|
||||
UPDATE template_node_configs
|
||||
SET rank = (
|
||||
@@ -127,6 +133,11 @@ function insertAndRankConfigs(db: any, configs: any[]) {
|
||||
`);
|
||||
|
||||
// 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(`
|
||||
DELETE FROM template_node_configs
|
||||
WHERE id NOT IN (
|
||||
@@ -294,6 +305,12 @@ async function fetchTemplates(
|
||||
|
||||
if (!hasMetadataColumn) {
|
||||
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(`
|
||||
ALTER TABLE templates ADD COLUMN metadata_json TEXT;
|
||||
ALTER TABLE templates ADD COLUMN metadata_generated_at DATETIME;
|
||||
|
||||
@@ -64,33 +64,54 @@ export class TemplateRepository {
|
||||
} else {
|
||||
// Create FTS5 virtual table
|
||||
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(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
|
||||
name, description, content=templates
|
||||
);
|
||||
`);
|
||||
|
||||
|
||||
// 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(`
|
||||
CREATE TRIGGER IF NOT EXISTS templates_ai AFTER INSERT ON templates BEGIN
|
||||
INSERT INTO templates_fts(rowid, name, description)
|
||||
VALUES (new.id, new.name, new.description);
|
||||
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(`
|
||||
CREATE TRIGGER IF NOT EXISTS templates_au AFTER UPDATE ON templates BEGIN
|
||||
UPDATE templates_fts SET name = new.name, description = new.description
|
||||
WHERE rowid = new.id;
|
||||
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(`
|
||||
CREATE TRIGGER IF NOT EXISTS templates_ad AFTER DELETE ON templates BEGIN
|
||||
DELETE FROM templates_fts WHERE rowid = old.id;
|
||||
END;
|
||||
`);
|
||||
|
||||
|
||||
logger.info('FTS5 support enabled for template search');
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -524,8 +545,14 @@ export class TemplateRepository {
|
||||
try {
|
||||
// Clear existing FTS data
|
||||
this.db.exec('DELETE FROM templates_fts');
|
||||
|
||||
|
||||
// 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(`
|
||||
INSERT INTO templates_fts(rowid, name, description)
|
||||
SELECT id, name, description FROM templates
|
||||
|
||||
Reference in New Issue
Block a user