Jul 23, 2026·7 min read·17 visits
Authenticated n8n users can execute arbitrary SQL commands on connected PostgreSQL databases by exploiting unescaped identifier interpolation within the PostgresTrigger node.
An authenticated SQL injection vulnerability (CWE-89) in n8n's PostgresTrigger node allows users with workflow creation or modification privileges to inject arbitrary SQL statements. Because n8n dynamically constructs database administration and event subscription queries by directly interpolating user-controlled parameters—such as PostgreSQL channel, function, and trigger names—without sanitization or identifier quoting, attackers can execute arbitrary queries within the context of the configured database credentials, potentially leading to unauthorized data exposure, system tampering, or remote code execution.
n8n relies on node integrations to perform a wide variety of actions, with the PostgresTrigger node facilitating continuous integration with PostgreSQL environments. This node acts as an event subscriber, monitoring target tables for activity such as row insertions, modifications, or deletions, and passing those events downstream to activate active workflows. To handle these events asynchronously, n8n must programmatically configure event listeners inside the target database schema. This operation requires executing Data Definition Language (DDL) and administrative operations dynamically.
The vulnerability resides in the way these administrative queries are assembled. Under normal operations, the application creates custom stored procedures, trigger bindings, and asynchronous publication channels on the fly. Because user input is accepted to specify identifiers such as the helper function name, trigger name, or notification channel, the application exposes an attack surface to anyone authorized to construct or modify workflows.
If an attacker can manipulate these properties, they can bypass the intended query parameters and insert raw SQL commands. Because these queries run with the privileges of the connected PostgreSQL server account, a SQL injection flaw in this component can result in a significant compromise of the downstream database server.
The underlying issue stems from a structural constraint in standard SQL databases and query drivers. Standard parameterized queries, which replace placeholders like $1 or ? with verified literal values, only support Data Manipulation Language (DML) fields, such as strings, integers, or dates inside a WHERE or VALUES clause. Database drivers cannot accept structural SQL identifiers, such as database names, table names, function names, trigger bindings, or logical operators, as parameters.
To construct DDL queries dynamically, applications must instead build the query string programmatically. When doing so, developers must sanitize, escape, or explicitly quote all user-controlled database identifiers before concatenating them into the final SQL command template. In vulnerable versions of n8n, this safety layer was absent.
Specifically, when configuring or running the PostgresTrigger node, n8n dynamically generated statements to declare trigger functions and bind them to tables. The engine performed direct string interpolation on identifier fields like the PostgreSQL channel name, trigger function name, and trigger identifier. Because the backend trusted these inputs without applying identifier quoting mechanics, arbitrary SQL payloads were inserted directly into the query execution pipeline.
Below is a conceptual representation of the vulnerable code pattern contrasted with the secure implementation designed for the patch.
In the vulnerable codebase, string interpolation was performed directly inside DDL templates, ignoring the structural nature of the variables:
// Vulnerable Implementation Pattern
async function setupPostgresTrigger(client, config) {
const functionName = config.functionName; // Unsanitised user input
const triggerName = config.triggerName; // Unsanitised user input
const tableName = config.tableName; // Schema identifier
// Directly interpolating user input into structural commands
const createFunctionQuery = `
CREATE OR REPLACE FUNCTION ${functionName}()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify('n8n_db_event', TG_TABLE_NAME);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`;
const createTriggerQuery = `
CREATE TRIGGER ${triggerName}
AFTER INSERT OR UPDATE ON ${tableName}
FOR EACH ROW EXECUTE FUNCTION ${functionName}();
`;
await client.query(createFunctionQuery);
await client.query(createTriggerQuery);
}In the patched versions, the application ensures that all user-provided structural identifiers are passed through a defensive escaping function. In PostgreSQL environments, identifiers must either be wrapped in double-quotes and have internal double-quotes doubled, or be processed using specific library utilities like pg-format (such as the %I format specifier):
// Patched Implementation Pattern
import pgFormat from 'pg-format';
async function setupPostgresTriggerPatched(client, config) {
// Utilising pgFormat.ident to properly escape SQL identifiers
const escapedFunctionName = pgFormat.ident(config.functionName);
const escapedTriggerName = pgFormat.ident(config.triggerName);
const escapedTableName = pgFormat.ident(config.tableName);
// The identifiers are safely enclosed in double-quotes, neutralising injection
const createFunctionQuery = pgFormat(`
CREATE OR REPLACE FUNCTION %I()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify('n8n_db_event', TG_TABLE_NAME);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`, config.functionName);
const createTriggerQuery = pgFormat(`
CREATE TRIGGER %I
AFTER INSERT OR UPDATE ON %I
FOR EACH ROW EXECUTE FUNCTION %I();
`, config.triggerName, config.tableName, config.functionName);
await client.query(createFunctionQuery);
await client.query(createTriggerQuery);
}This structural shift from direct string interpolation to strict identifier formatting ensures that any injected structural symbols are interpreted strictly as literal components of the identifier name, thereby neutralizing the SQL syntax breakout.
Exploitation of this vulnerability requires network access to the n8n application interface and a set of active, low-privilege credentials capable of creating or editing workflows. The attacker must also target a workflow that contains (or is configured to contain) a valid PostgreSQL connection.
The following diagram illustrates the lifecycle of an exploitation attempt:
To execute an attack, the adversary inputs a crafted string into the Function Name property of the PostgresTrigger node configuration. For instance, by supplying a payload containing a semicolon followed by a secondary command, the engine divides the execution block:
payload_func(); DROP TABLE application_users; --When the workflow is saved, activated, or tested, n8n sends the command to the PostgreSQL server. The database parser interprets the input as follows:
It processes the initial CREATE OR REPLACE FUNCTION payload_func()... command.
It detects the semicolon, closing the initial declaration context.
It treats the next sequence, DROP TABLE application_users, as a separate, subsequent statement, executing it immediately.
It treats the double hyphens (--) as a comment, ignoring any leftover structural syntax from the original template.
The direct impact of successful exploitation is defined by the capabilities and permissions of the database user configured inside n8n. If the node runs with basic read and write permissions, the attacker can manipulate, delete, or exfiltrate all tables within the accessible schemas. This can compromise credential stores, transactional data, or operational parameters.
If the configured database account possesses elevated administrative privileges or is a SUPERUSER, the risk surface expands significantly. Under these conditions, an attacker can pivot from SQL execution to system-level commands on the underlying host operating system. This is frequently achieved using PostgreSQL-specific functionalities like COPY ... TO PROGRAM or administrative extensions:
-- Command execution template via COPY TO PROGRAM
COPY (SELECT 1) TO PROGRAM 'id > /tmp/out';The CVSS v4.0 assessment registers a base score of 5.8. Although the initial impact to n8n itself is low since the host container or server environment is separate, the subsequent impact to the database infrastructure is classified as high across confidentiality, integrity, and availability.
The primary and recommended action is to update the n8n deployment to a patched release. Ensure your platform is aligned with one of the following official versions:
Version 1.123.67 or later (for 1.x installations)
Version 2.31.5 or later (for 2.x stable installations)
Version 2.32.1 or later (for 2.x latest installations)
If immediate upgrading is not possible, the PostgresTrigger node can be deactivated globally to eliminate the vulnerability's attack surface. To do this, configure the NODES_EXCLUDE environment variable in the host system configuration:
export NODES_EXCLUDE="n8n-nodes-base.postgresTrigger"Furthermore, apply the principle of least privilege to all database connections configured in n8n. Ensure that the database users assigned to these credentials do not have SUPERUSER privileges, and explicitly restrict their abilities to create, modify, or execute procedures outside of the minimum functional scope required for the integration.
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:H| Product | Affected Versions | Fixed Version |
|---|---|---|
n8n n8n-io | < 1.123.67 | 1.123.67 |
n8n n8n-io | >= 2.0.0-rc.0, < 2.31.5 | 2.31.5 |
n8n n8n-io | >= 2.32.0, < 2.32.1 | 2.32.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-89 |
| Attack Vector | Network (Low Privileges Required) |
| CVSS Score | 5.8 |
| Exploit Status | None (No verified public proof of concept) |
| Impact | Database Compromise / Potential Remote Code Execution |
| KEV Status | Not Listed |
The software constructs an SQL command using externally-influenced input, but does not neutralize or incorrectly neutralizes elements that could modify the intended SQL command.
A path traversal vulnerability (CWE-22) in Contao CMS allows unauthenticated remote attackers to bypass directory boundary restrictions in ImagesController and access files within the project directory.
Contao Open Source CMS versions 4.0.0 through 5.3.49 and 5.4.0-RC1 through 5.7.11 contain a Cross-Site Request Forgery (CSRF) vulnerability in backend parameter handling. The `RequestTokenListener` component validates anti-CSRF tokens solely for HTTP POST requests, while GET-based declarative guards run only when an `act` parameter is present in the query string. Consequently, custom backend actions dispatched via alternative parameters such as `key=` can execute without CSRF token verification when triggered by an authenticated user.
Contao CMS versions 4.1.0 through 5.3.49 and 5.4.0-RC1 through 5.7.11 fail to validate form submission tokens and enforce rate limiting when processing activation email resend requests via HTTP POST, enabling resource exhaustion and account state enumeration.
An information disclosure vulnerability in Contao CMS allows unauthenticated site visitors to view protected page titles, URLs, and text excerpts through search queries when protected page indexing is disabled after previously being enabled.
In Vikunja prior to version 2.6.0, relation creation via the CalDAV endpoint fails to invoke the TaskRelation.CanCreate authorization check. This missing access control allows an authenticated user to establish unauthorized relationships and perform write operations against any task, provided its unique identifier (UID) is known.
A cross-project information disclosure vulnerability in Vikunja allows authenticated users with read access to one project to view private task details from unauthorized projects via subtask expansion parameters.