diff --git a/templated/templateddetector/plugins/exposedui/N8N_ExposedRestApi.textproto b/templated/templateddetector/plugins/exposedui/N8N_ExposedRestApi.textproto new file mode 100644 index 000000000..c6c070757 --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/N8N_ExposedRestApi.textproto @@ -0,0 +1,118 @@ +# proto-file: proto/templated_plugin.proto +# proto-message: TemplatedPlugin + +############### +# PLUGIN INFO # +############### + +info: { + type: VULN_DETECTION + name: "N8N_ExposedRestApi" + author: "yuradoc (yuradoc.research@gmail.com)" + version: "1.0" +} + +finding: { + main_id: { + publisher: "GOOGLE" + value: "N8N_EXPOSED_REST_API" + } + title: "Exposed n8n REST API" + description: + "The n8n automation platform instance exposes its REST API without proper " + "authentication, allowing unauthenticated access to workflow management " + "functionality. This may enable attackers to enumerate, modify, and execute " + "workflows, including those containing built-in command execution nodes, " + "potentially leading to remote code execution. Exposure conditions vary " + "across n8n versions. Early releases without user management did not " + "consistently protect REST endpoints, and available authentication mechanisms " + "(e.g., JWT or basic auth) may not fully cover all /rest/* routes. In later " + "0.x versions, authentication behavior may be inconsistent—for example, " + "instances without an initialized owner account or with ineffective " + "configuration flags may allow unauthenticated access to /rest/* endpoints." + recommendation: + "Upgrade the n8n instance to a recent supported version where authentication " + "is consistently enforced by default. Older 0.x versions may not reliably " + "protect REST API endpoints. Additionally, ensure authentication is properly " + "configured and that the instance is not exposed to untrusted networks. Verify " + "that all REST API endpoints are protected, especially in partially initialized " + "deployments. Refer to https://docs.n8n.io/hosting/securing/overview/ for guidance." + severity: CRITICAL +} + +########### +# ACTIONS # +########### + +actions: { + name: "n8n_vuln_fingerprint" + http_request: { + method: GET + uri: "/rest/settings" + response: { + http_status: 200 + expect_all: { + conditions: [ + { body: {} contains: '"urlBaseWebhook"' }, + { body: {} contains: '"versionCli":"0.' } + ] + } + } + } +} + +# Gets the cookie if available, still passes this step if not available. +actions: { + name: "n8n_retrieve_auth_cookie" + http_request: { + method: GET + uri: "/rest/login" + response: { + extract_any: { + patterns: [ + { + from_header: { name: "Set-Cookie" } + regexp: "(n8n-auth=[^;]+)" + variable_name: "n8n_auth_token" + }, + { + from_body: {} + regexp: "()" + } + ] + } + } + } +} + +# Gets workflows, sending the previous cookie if it was available. +actions: { + name: "n8n_retrieve_existing_flows" + http_request: { + method: GET + uri: "/rest/workflows" + headers: [ + { name: "Cookie" value: "{{ n8n_auth_token }}" } + ] + response: { + http_status: 200 + expect_all: { + conditions: [ + { body: {} contains: '"data":[' } + ] + } + } + } +} + +############# +# WORKFLOWS # +############# + +workflows: { + actions: [ + "n8n_vuln_fingerprint", + "n8n_retrieve_auth_cookie", + "n8n_retrieve_existing_flows" + ] +} diff --git a/templated/templateddetector/plugins/exposedui/N8N_ExposedRestApi_test.textproto b/templated/templateddetector/plugins/exposedui/N8N_ExposedRestApi_test.textproto new file mode 100644 index 000000000..f80ede8c9 --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/N8N_ExposedRestApi_test.textproto @@ -0,0 +1,114 @@ +# proto-file: proto/templated_plugin_tests.proto +# proto-message: TemplatedPluginTests + +config: { + tested_plugin: "N8N_ExposedRestApi" +} + +tests: { + name: "whenVulnerable_noOwner_returnsTrue" + expect_vulnerability: true + + mock_http_server: { + mock_responses: [ + { + uri: "/rest/settings" + status: 200 + body_content: '{"data":{"urlBaseWebhook":"http://target:5678/","versionCli":"0.237.0",...}}' + }, + { + uri: "/rest/login" + status: 200 + headers: [ + { name: "Set-Cookie" value: "n8n-auth=jwt_token" } + ] + }, + { + uri: "/rest/workflows" + status: 200 + condition: { + headers: [ + { name: "Cookie" value: "n8n-auth=jwt_token" } + ] + } + body_content: '{"data":[{"id":"Q2cpznBmI6NCfeBF","name":"Vulnerable Workflow",...},...]}' + } + ] + } +} + +tests: { + name: "whenVulnerable_noUserManagement_returnsTrue" + expect_vulnerability: true + + mock_http_server: { + mock_responses: [ + { + uri: "/rest/settings" + status: 200 + body_content: '{"data":{"urlBaseWebhook":"http://target:5678/","versionCli":"0.54.0",...}}' + }, + { + uri: "/rest/login" + status: 404 + }, + { + uri: "/rest/workflows" + status: 200 + body_content: '{"data":[{"id":"1","name":"Legacy Workflow",...},...]}' + } + ] + } +} + +tests: { + name: "whenNotVulnerable_restProtected_returnsFalse" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "/rest/settings" + status: 200 + body_content: '{"data":{"urlBaseWebhook":"http://target:5678/","versionCli":"0.237.0",...}}' + }, + { + uri: "/rest/login" + status: 401 + }, + { + uri: "/rest/workflows" + status: 401 + } + ] + } +} + +tests: { + name: "whenNotVulnerable_n8nModern_returnsFalse" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "/rest/settings" + status: 200 + body_content: '{"data":{"instanceId":"...","versionCli":"1.123.21"}}' + } + ] + } +} + +tests: { + name: "whenNotN8n_returnsFalse" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "/rest/settings" + status: 404 + } + ] + } +}