From e2c2eb21cb3ef9e7098537b1ccbd0af05b67255a Mon Sep 17 00:00:00 2001 From: Jeppe B <2jepp9350@gmail.com> Date: Tue, 2 Jun 2026 00:32:35 +0200 Subject: [PATCH] Harden n8n webhook trigger URL validation --- services/nginx/app/classes/n8n.php | 39 +++++++++++++++++-- .../N8n/N8nWebhookUrlValidationWiringTest.php | 13 +++++++ 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 services/nginx/app/tests/Unit/N8n/N8nWebhookUrlValidationWiringTest.php diff --git a/services/nginx/app/classes/n8n.php b/services/nginx/app/classes/n8n.php index ef60119c..f929f7bb 100644 --- a/services/nginx/app/classes/n8n.php +++ b/services/nginx/app/classes/n8n.php @@ -340,18 +340,49 @@ class n8n implements n8n_i throw new Exception('Webhook target must not be empty.'); } - if (filter_var($target, FILTER_VALIDATE_URL) !== false) { - return $target; - } - $baseUrl = trim((string)$this->config->webhook_base_url->getVariableValue()); if ($baseUrl === '') { throw new Exception('n8n webhook base URL is not configured.'); } + if (filter_var($target, FILTER_VALIDATE_URL) !== false) { + if (!$this->isAllowedWebhookAbsoluteUrl($target, $baseUrl)) { + throw new Exception('Webhook URL must use the configured n8n webhook host.'); + } + + return $target; + } + return rtrim($baseUrl, '/') . '/' . ltrim($target, '/'); } + private function isAllowedWebhookAbsoluteUrl(string $targetUrl, string $baseUrl): bool + { + $targetParts = parse_url($targetUrl); + $baseParts = parse_url($baseUrl); + + if (!is_array($targetParts) || !is_array($baseParts)) { + return false; + } + + $targetHost = strtolower((string)($targetParts['host'] ?? '')); + $baseHost = strtolower((string)($baseParts['host'] ?? '')); + if ($targetHost === '' || $baseHost === '' || $targetHost !== $baseHost) { + return false; + } + + $targetScheme = strtolower((string)($targetParts['scheme'] ?? '')); + $baseScheme = strtolower((string)($baseParts['scheme'] ?? '')); + if ($targetScheme === '' || $baseScheme === '' || $targetScheme !== $baseScheme) { + return false; + } + + $targetPort = (int)($targetParts['port'] ?? ($targetScheme === 'https' ? 443 : 80)); + $basePort = (int)($baseParts['port'] ?? ($baseScheme === 'https' ? 443 : 80)); + + return $targetPort === $basePort; + } + /** * @throws Exception */ diff --git a/services/nginx/app/tests/Unit/N8n/N8nWebhookUrlValidationWiringTest.php b/services/nginx/app/tests/Unit/N8n/N8nWebhookUrlValidationWiringTest.php new file mode 100644 index 00000000..14520d52 --- /dev/null +++ b/services/nginx/app/tests/Unit/N8n/N8nWebhookUrlValidationWiringTest.php @@ -0,0 +1,13 @@ +not->toBeFalse(); + expect($content)->toContain('isAllowedWebhookAbsoluteUrl'); + expect($content)->toContain('Webhook URL must use the configured n8n webhook host.'); + expect($content)->toContain("$targetHost !== $baseHost"); + expect($content)->toContain("$targetScheme !== $baseScheme"); + expect($content)->toContain('return $targetPort === $basePort;'); +});