@n8n/backend-network 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +135 -4
  2. package/dist/build.tsbuildinfo +1 -1
  3. package/dist/http/axios/invoke.js +3 -5
  4. package/dist/http/axios/invoke.js.map +1 -1
  5. package/dist/http/axios/request.js +2 -2
  6. package/dist/http/axios/utils.js +5 -4
  7. package/dist/http/axios/utils.js.map +1 -1
  8. package/dist/http/client-default-headers.d.ts +1 -1
  9. package/dist/http/client-default-headers.js +17 -6
  10. package/dist/http/client-default-headers.js.map +1 -1
  11. package/dist/http/client-options.d.ts +1 -0
  12. package/dist/http/client-request-error.d.ts +1 -0
  13. package/dist/http/client-request-error.js +6 -0
  14. package/dist/http/client-request-error.js.map +1 -1
  15. package/dist/http/fake-outbound-http.d.ts +30 -0
  16. package/dist/http/fake-outbound-http.js +77 -0
  17. package/dist/http/fake-outbound-http.js.map +1 -0
  18. package/dist/http/http-proxy.d.ts +0 -6
  19. package/dist/http/http-proxy.js +2 -33
  20. package/dist/http/http-proxy.js.map +1 -1
  21. package/dist/http/index.d.ts +4 -3
  22. package/dist/http/index.js +6 -4
  23. package/dist/http/index.js.map +1 -1
  24. package/dist/http/outbound-http.d.ts +9 -5
  25. package/dist/http/outbound-http.js +1 -1
  26. package/dist/http/outbound-http.js.map +1 -1
  27. package/dist/http/undici/transport.js +9 -5
  28. package/dist/http/undici/transport.js.map +1 -1
  29. package/dist/proxy/index.d.ts +2 -0
  30. package/dist/proxy/index.js +11 -0
  31. package/dist/proxy/index.js.map +1 -0
  32. package/dist/proxy/no-proxy-patch.d.ts +1 -0
  33. package/dist/proxy/no-proxy-patch.js +71 -0
  34. package/dist/proxy/no-proxy-patch.js.map +1 -0
  35. package/dist/proxy/proxy-resolution.d.ts +6 -0
  36. package/dist/proxy/proxy-resolution.js +42 -0
  37. package/dist/proxy/proxy-resolution.js.map +1 -0
  38. package/dist/ssrf/hostname-matcher.js +2 -1
  39. package/dist/ssrf/hostname-matcher.js.map +1 -1
  40. package/dist/ssrf/index.d.ts +1 -0
  41. package/dist/ssrf/index.js +3 -1
  42. package/dist/ssrf/index.js.map +1 -1
  43. package/dist/ssrf/ssrf-blocked-hostname.error.d.ts +5 -0
  44. package/dist/ssrf/ssrf-blocked-hostname.error.js +18 -0
  45. package/dist/ssrf/ssrf-blocked-hostname.error.js.map +1 -0
  46. package/dist/ssrf/ssrf-protection.service.d.ts +3 -1
  47. package/dist/ssrf/ssrf-protection.service.js +15 -1
  48. package/dist/ssrf/ssrf-protection.service.js.map +1 -1
  49. package/dist/testing.d.ts +1 -0
  50. package/dist/testing.js +3 -1
  51. package/dist/testing.js.map +1 -1
  52. package/package.json +18 -9
package/README.md CHANGED
@@ -4,12 +4,143 @@ The single home for n8n's backend outbound-network concerns.
4
4
 
5
5
  ## Why this package exists
6
6
 
7
- Today network behavior is scattered across `packages/core` and several `@n8n/*` packages.
7
+ Today network behavior is scattered across `packages/core` and several `@n8n/*` packages.
8
8
 
9
- This package consolidates into one place behind a single factory contract:
9
+ This package consolidates into one place behind a single factory contract:
10
10
  SSRF/DNS guarding, proxy handling, and the HTTP client plumbing.
11
11
  The eventual goal is to make backend network behavior reviewable and controllable from a single entry point.
12
12
 
13
- ## Roadmap
13
+ ## Using `OutboundHttp`
14
14
 
15
- Tracked in [CAT-3365](https://linear.app/n8n/issue/CAT-3365/unify-http-concerns-scope-the-backend-network-layer-into-n8nbackend).
15
+ `OutboundHttp` is the single entry point for outbound HTTP in the backend. It is
16
+ injectable via `@n8n/di`, so depend on it from a constructor rather than
17
+ constructing it yourself:
18
+
19
+ ```ts
20
+ import { OutboundHttp } from '@n8n/backend-network';
21
+ import { Service } from '@n8n/di';
22
+
23
+ @Service()
24
+ class MyProvider {
25
+ constructor(private readonly outboundHttp: OutboundHttp) {}
26
+ }
27
+ ```
28
+
29
+ Pick the method by intent, not by transport library:
30
+
31
+ ### `requests()` — you make the call
32
+
33
+ Use when your code drives the request and consumes the response (the n8n request
34
+ pipeline). Returns a `HttpRequestClient`:
35
+
36
+ ```ts
37
+ const client = this.outboundHttp.requests();
38
+ const data = await client.request({ method: 'GET', url: 'https://api.example.com/v1/data' });
39
+ ```
40
+
41
+ `HttpRequestClientOptions` carry policy that applies to every call on the
42
+ client — SSRF level, a `baseURL`, and default `headers`. Set them once at
43
+ creation instead of repeating them per request:
44
+
45
+ ```ts
46
+ const client = this.outboundHttp.requests({
47
+ baseURL: 'https://api.example.com',
48
+ headers: () => ({ Authorization: `Bearer ${this.token}` }),
49
+ });
50
+ ```
51
+
52
+ ### `transport()` — a third-party SDK makes the call
53
+
54
+ Use when you hand transport primitives to a library you do not drive yourself
55
+ (an OpenAI/Anthropic SDK, the AWS SDK, an OIDC client). `HttpTransport` exposes
56
+ `asCustomFetch()`, `getDispatcher()`, and `getNodeAgent()`; which one you use is
57
+ dictated by what the consuming library accepts.
58
+
59
+ ## SSRF protection
60
+
61
+ ### On by default
62
+
63
+ Both `requests()` and `transport()` apply the container's
64
+ `SsrfProtectionService` **unless you explicitly opt out** with `ssrf: 'disabled'`.
65
+
66
+ This default is deliberate. Outbound HTTP in n8n is frequently driven by
67
+ user-controlled input — credential URLs, workflow parameters, redirect targets
68
+ returned by a remote server. Without guarding, that input can be pointed at
69
+ internal-only addresses (cloud metadata endpoints, `localhost`, private ranges),
70
+ turning the n8n backend into a confused deputy (a Server-Side Request Forgery,
71
+ or SSRF). Centralizing the call here means every backend request — and every
72
+ redirect hop — is validated by default, so a new call site is safe unless
73
+ someone consciously turns the guard off.
74
+
75
+ Because the secure default lives in the factory rather than at each call site,
76
+ forgetting to add protection cannot silently introduce a vulnerability — the
77
+ unsafe choice is the explicit one.
78
+
79
+ ### Why the choice is explicit per call, not read from config
80
+
81
+ The `ssrf` option is passed **per call**; it is **not** derived automatically
82
+ from `SsrfProtectionConfig`. That is intentional: whether a request is dangerous
83
+ is a property of *its destination*, and only the calling code knows that. A
84
+ global flag cannot tell a fixed, n8n-owned URL apart from a URL a user just
85
+ pasted into a credential. So each call site makes a single, local, reviewable
86
+ decision — and a reviewer reading a `ssrf: 'disabled'` knows to ask "is this
87
+ destination really not user-controlled?".
88
+
89
+ `SsrfProtectionConfig` (env-driven) configures *how* the guard behaves once it
90
+ runs — the blocked/allowed IP ranges (`N8N_SSRF_BLOCKED_IP_RANGES`,
91
+ `N8N_SSRF_ALLOWED_IP_RANGES`), the allowed and blocked hostnames
92
+ (`N8N_SSRF_ALLOWED_HOSTNAMES`, `N8N_SSRF_BLOCKED_HOSTNAMES`), and the DNS-cache
93
+ size. Its `enabled` flag (`N8N_SSRF_PROTECTION_ENABLED`) is the **instance-wide gate that high-risk call sites consult** to decide whether to turn the guard on (see below).
94
+
95
+ The config sets the policy; the call site decides whether that policy applies to *this* destination.
96
+
97
+ ### Choosing an SSRF level: low-risk vs high-risk calls
98
+
99
+ Classify the **destination**, then pick:
100
+
101
+ | Destination | Risk | What to pass |
102
+ | --- | --- | --- |
103
+ | Fixed n8n-owned host, or a fixed public vendor API (Slack, Linear, npm registry default, AWS service endpoint) | **Low** — not user-controllable | `ssrf: 'disabled'` + a one-line "fixed host" comment |
104
+ | Admin-configured infrastructure that may legitimately be internal (SAML/OIDC IdP, OTLP collector, log-streaming destination, external-secrets manager) | **Low–medium** — operator-trusted | `ssrf: 'disabled'` + a "may point at internal X" comment. The `N8N_SSRF_ALLOWED_*` allowlists are the escape hatch when the instance runs with protection globally on. |
105
+ | User- or remote-controlled URL (workflow import URL, credential/OAuth URLs, a discovery document's second hop, a user-supplied registry, an LLM/web-research target) | **High** — attacker-influenceable | gate on the instance setting: `ssrf: ssrfConfig.enabled ? ssrfProtectionService : 'disabled'` |
106
+
107
+ ```ts
108
+ // LOW risk — fixed, n8n-owned host.
109
+ const client = this.outboundHttp.requests({ ssrf: 'disabled' });
110
+
111
+ // HIGH risk — `url` comes from user input. Guard when the instance enables it.
112
+ const client = this.outboundHttp.requests({
113
+ ssrf: this.ssrfConfig.enabled ? this.ssrfProtectionService : 'disabled',
114
+ });
115
+ ```
116
+
117
+ Whatever you pick, **write a one-line comment stating why** — that comment is
118
+ what a security reviewer reads. Reference implementations:
119
+ `packages/cli/src/oauth/oauth.service.ts` (config-gated high-risk) and
120
+ `packages/nodes-base/credentials/common/token-request.ts`, which lifts the
121
+ low/high-risk choice into the type system with a
122
+ `'fixed-vendor' | 'user-controlled'` parameter so the caller is forced to
123
+ classify the destination.
124
+
125
+ ## The boundary rule
126
+
127
+ The `n8n-local-rules/no-uncentralized-http` ESLint rule enforces this.
128
+ It is on by default for every Node backend package.
129
+
130
+ Two sanctioned escape hatches, depending on the shape of the exception:
131
+
132
+ **1. Inline disable** When a single callsite legitimately cannot use the factory, disable the
133
+ rule on the line with a justifying comment:
134
+
135
+ ```ts
136
+ // eslint-disable-next-line n8n-local-rules/no-uncentralized-http -- <reason>
137
+ import axios from 'axios';
138
+ ```
139
+
140
+ Always include the reason after `--`.
141
+
142
+ **2. Central allow list** For whole packages that are out of scope, add the file
143
+ path (a substring of the absolute path is enough) to the `allow` list in
144
+ `packages/@n8n/eslint-config/src/configs/backend-network-boundary.ts`.
145
+
146
+ Keep the list shrinking: every entry is debt or a documented carve-out, not a default.