fix: resolve HTTP server URL handling and security issues (#41, #42)

- Add intelligent URL detection supporting BASE_URL, PUBLIC_URL, and proxy headers
- Fix hardcoded localhost URLs in server console output
- Add hostname validation to prevent host header injection attacks
- Restrict URL schemes to http/https only (block javascript:, file://, etc.)
- Remove sensitive environment data from API responses
- Add GET endpoints (/, /mcp) for better API discovery
- Fix version inconsistency between server implementations
- Update HTTP bridge to use HOST/PORT environment variables
- Add comprehensive test scripts for URL configuration and security

This resolves issues #41 and #42 by making the HTTP server properly handle
deployment behind reverse proxies and adds critical security validations.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-07-15 16:46:30 +02:00
parent 4c217088f5
commit a0f09fba28
12 changed files with 641 additions and 8 deletions

View File

@@ -8,7 +8,10 @@
const http = require('http');
const readline = require('readline');
const MCP_URL = process.env.MCP_URL || 'http://localhost:3000/mcp';
// Use MCP_URL from environment or construct from HOST/PORT if available
const defaultHost = process.env.HOST || 'localhost';
const defaultPort = process.env.PORT || '3000';
const MCP_URL = process.env.MCP_URL || `http://${defaultHost}:${defaultPort}/mcp`;
const AUTH_TOKEN = process.env.AUTH_TOKEN || process.argv[2];
if (!AUTH_TOKEN) {

96
scripts/test-security.ts Normal file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env node
import axios from 'axios';
import { spawn } from 'child_process';
async function testMaliciousHeaders() {
console.log('🔒 Testing Security Fixes...\n');
// Start server with TRUST_PROXY enabled
const serverProcess = spawn('node', ['dist/mcp/index.js'], {
env: {
...process.env,
MCP_MODE: 'http',
AUTH_TOKEN: 'test-security-token-32-characters-long',
PORT: '3999',
TRUST_PROXY: '1'
}
});
// Wait for server to start
await new Promise(resolve => {
serverProcess.stdout.on('data', (data) => {
if (data.toString().includes('Press Ctrl+C to stop')) {
resolve(undefined);
}
});
});
const testCases = [
{
name: 'Valid proxy headers',
headers: {
'X-Forwarded-Host': 'example.com',
'X-Forwarded-Proto': 'https'
}
},
{
name: 'Malicious host header (with path)',
headers: {
'X-Forwarded-Host': 'evil.com/path/to/evil',
'X-Forwarded-Proto': 'https'
}
},
{
name: 'Malicious host header (with @)',
headers: {
'X-Forwarded-Host': 'user@evil.com',
'X-Forwarded-Proto': 'https'
}
},
{
name: 'Invalid hostname (multiple dots)',
headers: {
'X-Forwarded-Host': '.....',
'X-Forwarded-Proto': 'https'
}
},
{
name: 'IPv6 address',
headers: {
'X-Forwarded-Host': '[::1]:3000',
'X-Forwarded-Proto': 'https'
}
}
];
for (const testCase of testCases) {
try {
const response = await axios.get('http://localhost:3999/', {
headers: testCase.headers,
timeout: 2000
});
const endpoints = response.data.endpoints;
const healthUrl = endpoints?.health?.url || 'N/A';
console.log(`${testCase.name}`);
console.log(` Response: ${healthUrl}`);
// Check if malicious headers were blocked
if (testCase.name.includes('Malicious') || testCase.name.includes('Invalid')) {
if (healthUrl.includes('evil.com') || healthUrl.includes('@') || healthUrl.includes('.....')) {
console.log(' ❌ SECURITY ISSUE: Malicious header was not blocked!');
} else {
console.log(' ✅ Malicious header was blocked');
}
}
} catch (error) {
console.log(`${testCase.name} - Request failed`);
}
console.log('');
}
serverProcess.kill();
}
testMaliciousHeaders().catch(console.error);

192
scripts/test-url-configuration.ts Executable file
View File

@@ -0,0 +1,192 @@
#!/usr/bin/env node
/**
* Test script for URL configuration in n8n-MCP HTTP server
* Tests various BASE_URL, TRUST_PROXY, and proxy header scenarios
*/
import axios from 'axios';
import { spawn } from 'child_process';
import { logger } from '../src/utils/logger';
interface TestCase {
name: string;
env: Record<string, string>;
expectedUrls?: {
health: string;
mcp: string;
};
proxyHeaders?: Record<string, string>;
}
const testCases: TestCase[] = [
{
name: 'Default configuration (no BASE_URL)',
env: {
MCP_MODE: 'http',
AUTH_TOKEN: 'test-token-for-testing-only',
PORT: '3001'
},
expectedUrls: {
health: 'http://localhost:3001/health',
mcp: 'http://localhost:3001/mcp'
}
},
{
name: 'With BASE_URL configured',
env: {
MCP_MODE: 'http',
AUTH_TOKEN: 'test-token-for-testing-only',
PORT: '3002',
BASE_URL: 'https://n8n-mcp.example.com'
},
expectedUrls: {
health: 'https://n8n-mcp.example.com/health',
mcp: 'https://n8n-mcp.example.com/mcp'
}
},
{
name: 'With PUBLIC_URL configured',
env: {
MCP_MODE: 'http',
AUTH_TOKEN: 'test-token-for-testing-only',
PORT: '3003',
PUBLIC_URL: 'https://api.company.com/mcp'
},
expectedUrls: {
health: 'https://api.company.com/mcp/health',
mcp: 'https://api.company.com/mcp/mcp'
}
},
{
name: 'With TRUST_PROXY and proxy headers',
env: {
MCP_MODE: 'http',
AUTH_TOKEN: 'test-token-for-testing-only',
PORT: '3004',
TRUST_PROXY: '1'
},
proxyHeaders: {
'X-Forwarded-Proto': 'https',
'X-Forwarded-Host': 'proxy.example.com'
}
},
{
name: 'Fixed HTTP implementation',
env: {
MCP_MODE: 'http',
USE_FIXED_HTTP: 'true',
AUTH_TOKEN: 'test-token-for-testing-only',
PORT: '3005',
BASE_URL: 'https://fixed.example.com'
},
expectedUrls: {
health: 'https://fixed.example.com/health',
mcp: 'https://fixed.example.com/mcp'
}
}
];
async function runTest(testCase: TestCase): Promise<void> {
console.log(`\n🧪 Testing: ${testCase.name}`);
console.log('Environment:', testCase.env);
const serverProcess = spawn('node', ['dist/mcp/index.js'], {
env: { ...process.env, ...testCase.env }
});
let serverOutput = '';
let serverStarted = false;
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
serverProcess.kill();
reject(new Error('Server startup timeout'));
}, 10000);
serverProcess.stdout.on('data', (data) => {
const output = data.toString();
serverOutput += output;
if (output.includes('Press Ctrl+C to stop the server')) {
serverStarted = true;
clearTimeout(timeout);
// Give server a moment to fully initialize
setTimeout(async () => {
try {
// Test root endpoint
const rootUrl = `http://localhost:${testCase.env.PORT}/`;
const rootResponse = await axios.get(rootUrl, {
headers: testCase.proxyHeaders || {}
});
console.log('✅ Root endpoint response:');
console.log(` - Endpoints: ${JSON.stringify(rootResponse.data.endpoints, null, 2)}`);
// Test health endpoint
const healthUrl = `http://localhost:${testCase.env.PORT}/health`;
const healthResponse = await axios.get(healthUrl);
console.log(`✅ Health endpoint status: ${healthResponse.data.status}`);
// Test MCP info endpoint
const mcpUrl = `http://localhost:${testCase.env.PORT}/mcp`;
const mcpResponse = await axios.get(mcpUrl);
console.log(`✅ MCP info endpoint: ${mcpResponse.data.description}`);
// Check console output
if (testCase.expectedUrls) {
const outputContainsExpectedUrls =
serverOutput.includes(testCase.expectedUrls.health) &&
serverOutput.includes(testCase.expectedUrls.mcp);
if (outputContainsExpectedUrls) {
console.log('✅ Console output shows expected URLs');
} else {
console.log('❌ Console output does not show expected URLs');
console.log('Expected:', testCase.expectedUrls);
}
}
serverProcess.kill();
resolve();
} catch (error) {
console.error('❌ Test failed:', error instanceof Error ? error.message : String(error));
serverProcess.kill();
reject(error);
}
}, 500);
}
});
serverProcess.stderr.on('data', (data) => {
console.error('Server error:', data.toString());
});
serverProcess.on('close', (code) => {
if (!serverStarted) {
reject(new Error(`Server exited with code ${code} before starting`));
} else {
resolve();
}
});
});
}
async function main() {
console.log('🚀 n8n-MCP URL Configuration Test Suite');
console.log('======================================');
for (const testCase of testCases) {
try {
await runTest(testCase);
console.log('✅ Test passed\n');
} catch (error) {
console.error('❌ Test failed:', error instanceof Error ? error.message : String(error));
console.log('\n');
}
}
console.log('✨ All tests completed');
}
main().catch(console.error);