Root cause
SSRF & network boundaries introduced
Fix webhook CORS issue by implementing backend proxy pattern
AI-assisted change: Claude
How AI contributed: Direct introductionPublished Aug 9, 2026 in
anubissbe/projecthub-mcp
Server-side request forgery in the Webhooks API of backend-fix/complete_backend.js. POST /api/webhooks and PUT /api/webhooks/:id store caller-supplied fields (including url) by spreading req.body into the sampleWebhooks array without any URL validation. Commit 31dd902 then made the backend issue outbound HTTP requests to that stored URL with Node's global fetch: the /api/webhooks/:id/test endpoint posts a test payload to fetch(webhook.url), and /api/webhooks/notify plus the task create/complete lifecycle hooks post event messages to every active subscribed webhook's url. A client able to reach the API (no auth middleware; CORS allows no-origin requests and any origin when CORS_ORIGIN=*) can register/point a webhook at arbitrary destinations such as localhost admin ports, RFC1918 services or a cloud metadata endpoint, turning the documented backend into an SSRF relay, with the response status/error details echoed back in the test endpoint response.
Root cause
Fix webhook CORS issue by implementing backend proxy pattern
AI-assisted change: Claude
Fix status
No minimum fix commit has been established for this finding.
Fix status unresolved
Case facts
@@ -428,6 +428,81 @@ const sampleWebhooks = [ } ]; +// Helper function to trigger webhook notifications+async function triggerWebhookNotification(event, data) {+ const activeWebhooks = sampleWebhooks.filter(w => + w.active && w.events.includes(event)+ );+ + for (const webhook of activeWebhooks) {+ try {+ let message;+ + // Format message based on event type+ switch (event) {+ case 'task.created':+ message = {+ text: `New task created: ${data.title}`,+ blocks: [{+ type: 'header',+ text: {+ type: 'plain_text',+ text: '🆕 New Task Created'+ }+ }, {+ type: 'section',+ fields: [+ {+ type: 'mrkdwn',+ text: `*Task:* ${data.title}`+ },+ {+ type: 'mrkdwn',+ text: `*Project:* ${data.projectName || 'N/A'}`+ },+ {+ type: 'mrkdwn',+ text: `*Priority:* ${data.priority || 'medium'}`+ },+ {+ type: 'mrkdwn',+ text: `*Created by:* ${data.createdBy || 'System'}`+ }+ ]+ }]+ };+ break;+ + case 'task.completed':+ message = {+ text: `Task completed: ${data.title}`,+ blocks: [{+ type: 'section',+ text: {+ type: 'mrkdwn',+ text: `✅ *Task Completed*\n"${data.title}" has been marked as complete!`+ }+ }]+ };+ break;+ + default:+ message = { text: `ProjectHub event: ${event}` };+ }+ + await fetch(webhook.url, {+ method: 'POST',+ headers: { 'Content-Type': 'application/json' },+ body: JSON.stringify(message)+ });+ + webhook.last_triggered = new Date().toISOString();+ } catch (error) {+ console.error(`Webhook notification error for ${webhook.name}:`, error);+ }+ }+}+ // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() });@@ -476,28 +551,69 @@ app.get('/api/projects/:id/tasks', (req, res) => { res.json(projectTasks); }); -app.post('/api/tasks', (req, res) => {+app.post('/api/tasks', async (req, res) => { const newTask = { id: `task-${Date.now()}`,@@ -0,0 +1,127 @@+#!/usr/bin/env node++// Test script for ProjectHub webhook integration+// This script tests the complete webhook flow: configuration, task creation, and notifications++const API_BASE = process.env.API_BASE || 'http://localhost:3010/api';++// Test webhook URL (you can replace this with your actual Slack webhook)+const TEST_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL || 'https://hooks.slack.com/services/TEST/TEST/TEST';++async function makeRequest(method, endpoint, body = null) {+ const options = {+ method,+ headers: {+ 'Content-Type': 'application/json'+ }+ };+ + if (body) {+ options.body = JSON.stringify(body);+ }+ + try {+ const response = await fetch(`${API_BASE}${endpoint}`, options);+ const data = await response.json();+ + if (!response.ok) {+ throw new Error(`HTTP ${response.status}: ${data.error || response.statusText}`);+ }+ + return data;+ } catch (error) {+ console.error(`Error in ${method} ${endpoint}:`, error.message);+ throw error;+ }+}++async function runTests() {+ console.log('🧪 Testing ProjectHub Webhook Integration\n');+ console.log(`API Base: ${API_BASE}`);+ console.log(`Test Webhook URL: ${TEST_WEBHOOK_URL}\n`);+ + try {+ // Step 1: Create a webhook configuration+ console.log('1️⃣ Creating webhook configuration...');+ const webhook = await makeRequest('POST', '/webhooks', {+ name: 'Test Slack Webhook',+ url: TEST_WEBHOOK_URL,+ events: ['task.created', 'task.completed'],+ active: true+ });+ console.log(`✅ Webhook created with ID: ${webhook.id}\n`);+ + // Step 2: Test the webhook+ console.log('2️⃣ Testing webhook connection...');+ try {+ const testResult = await makeRequest('POST', `/webhooks/${webhook.id}/test`);+ console.log('✅ Webhook test:', testResult.message);+ } catch (error) {+ console.log('⚠️ Webhook test failed (expected if using test URL):', error.message);+ }+ console.log('');+ + // Step 3: Create a project+ console.log('3️⃣ Creating test project...');+ const project = await makeRequest('POST', '/projects', {+ name: 'Webhook Test Project',+ description: 'Testing webhook integration',+ status: 'active',+ priority: 'high'+ });+ console.log(`✅ Project created: ${project.name} (ID: ${project.id})\n`);+ + // Step 4: Create a task (should trigger webhook)+ console.log('4️⃣ Creating task (should trigger webhook)...');+ const task = await makeRequest('POST', '/tasks', {+ title: 'Test Task for Webhook',+ description: 'This task should trigger a webhook notification',+ project_id: project.id,+ status: 'pending',+ priority: 'high',+ assignee: 'Test User'+ });+ console.log(`✅ Task created: ${task.title} (ID: ${task.id})`);+ console.log(' 📨 Webhook notification should have been sent for task.created\n');+ + // Step 5: Complete the task (should trigger another webhook)+ console.log('5️⃣ Completing task (should trigger webhook)...');+ await new Promise(resolve => setTimeout(resolve, 1000)); // Small delay@@ -681,26 +681,14 @@ function projectHub() { // Test webhook async testWebhook(webhook) {- // Send a test payload to the webhook URL+ // Test webhook through backend API to avoid CORS issues try {- const response = await fetch(webhook.url, {- method: 'POST',- headers: {- 'Content-Type': 'application/json',- 'X-ProjectHub-Event': 'test'- },- body: JSON.stringify({- event: 'test',- webhook_id: webhook.id,- timestamp: new Date().toISOString(),- message: 'This is a test webhook event from ProjectHub'- })- });+ const result = await api.post(`/webhooks/${webhook.id}/test`); - if (response.ok) {- this.showNotification('Webhook test successful!', 'success');+ if (result.success) {+ this.showNotification('Webhook test successful! Check your Slack channel.', 'success'); } else {- this.showNotification(`Webhook test failed: ${response.status}`, 'error');+ this.showNotification(`Webhook test failed: ${result.error || 'Unknown error'}`, 'error'); } } catch (error) { this.showNotification(`Webhook test failed: ${error.message}`, 'error');