CodeVetter · Repo Unpack · Risk map
n8n-io/n8n
Security-sensitive surface: community-package npm installs shell out via execFile; credential secrecy relies on Cipher key handling and N8N_ENCRYPTION_KEY; expression evaluation and JS/Python task runners execute user code (Tournament sandbox + prototype hardening + spawned processes); webhook/form routes intentionally serve user HTML outside the CSP middleware. The 3.1k-line WorkflowExecute and the multi-stage Start.init ordering are blast-radius hotspots, as are Redis pub/sub trust and the Postgres migration advisory lock.
- Community packages install third-party code at runtime: community-packages.service uses execFile/executeNpmCommand (npm pack/install) against registry.npmjs.org by default; verifyIntegrity and a vetted-package list mitigate but the attack surface (supply chain, postinstall) is inherent.
- LoadNodesAndCredentials mutates global module resolution: it rewrites NODE_PATH and calls node's private module._initPaths() so community packages resolve — fragile across Node upgrades and a code-loading vector for anything on NODE_PATH.
- Credential encryption depends on the instance key: Cipher.decryptV2 throws UnexpectedError('Encryption key not found') for unknown keyIds and silently treats unprefixed data as legacy CBC; wrong/missing N8N_ENCRYPTION_KEY renders all stored credentials unreadable.
- WorkflowExecute is a 3168-line stateful class; its run() must remain non-async to preserve PCancelable cancellation (explicit comment), meaning a well-meaning refactor breaks execution cancellation.
- Webhook/form routes are registered before bodyParser AND before the CSP middleware on purpose — user-authored HTML responses must not get the instance CSP; the comment warns that reordering 'silently changes' the security posture.
- Preset-credentials endpoint (/endpointPresetCredentials) can be configured without the overwrite middleware; in that case credentials can only be set once per boot — a misconfiguration could leave a credential-injection route unauthenticated.
- Expression evaluation is user-controlled code: Tournament evaluator + expressionSandboxHooks exist to contain it; any escape in the proxy/hooks layer is direct RCE inside the n8n process. (inferred)
- JS task runners spawn 'node' subprocesses (JsTaskRunnerProcess.startProcess) with env-injected grant tokens; N8N_RUNNERS_INSECURE_MODE exists in config, and runners authenticate to a broker port — misconfiguration widens code-exec blast radius.
- Auth: session cookie is a JWT (AUTH_COOKIE_NAME) bound to a browserId header and MFA-gated; ApiKeyAuthStrategy is evaluated before cookie auth so a wrong explicit key fails fast; revocation uses InvalidAuthToken entity.
- Postgres migrations run inside an advisory lock scoped by schema+prefix hash with transaction:'none' — all pending migrations commit or roll back as one unit, so a mid-migration crash can leave a partially migrated schema for every co-starting instance.
- Redis is a trust boundary in queue mode: commands flow over pub/sub channels to workers and webhooks relay through the same fabric; a compromised Redis can drive executions on all mains/workers. (inferred)
- Startup ordering is contractually fragile: entities must be registered before DataSource creation (loadModules), PubSubRegistry.init() must be re-invoked after modules/community-packages register @OnPubSubEvent handlers, and JwtService/BinaryDataConfig init require DeploymentKeyRepository — comments flag each of these ordering constraints.
- External secrets feature pulls secrets from AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, 1Password (SDK deps present); provider credentials and sync are handled in modules/external-secrets.ee — a leak here exposes all downstream credentials.