From 1fa68232c30808dd9a6e33fb64f37ee4ce27d94c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Mar 2022 14:42:54 -0400 Subject: [PATCH] set address header explicitly --- packages/adapter-node/README.md | 29 +++++++- packages/adapter-node/index.d.ts | 3 +- packages/adapter-node/index.js | 6 +- .../adapter-node/src/get-client-address.js | 72 ------------------- packages/adapter-node/src/handler.d.ts | 3 +- packages/adapter-node/src/handler.js | 25 +++++-- 6 files changed, 54 insertions(+), 84 deletions(-) delete mode 100644 packages/adapter-node/src/get-client-address.js diff --git a/packages/adapter-node/README.md b/packages/adapter-node/README.md index e462355c5926..e301cbe7fb58 100644 --- a/packages/adapter-node/README.md +++ b/packages/adapter-node/README.md @@ -26,7 +26,7 @@ export default { host: 'HOST_HEADER' } }, - trustProxy: false + xForwardedForIndex: -1 }) } }; @@ -64,6 +64,14 @@ PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build > [`x-forwarded-proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) and [`x-forwarded-host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) are de facto standard headers that forward the original protocol and host if you're using a reverse proxy (think load balancers and CDNs). You should only set these variables if you trust the reverse proxy. +The [RequestEvent](https://kit.svelte.dev/docs/types#additional-types-requestevent) object passed to hooks and endpoints includes an `event.clientAddress` property representing the client's IP address. By default this is the connecting `remoteAddress`. If your server is behind one or more proxies (such as a load balancer), this value will contain the innermost proxy's IP address rather than the client's, so we need to specify an `ADDRESS_HEADER` to read the address from: + +``` +ADDRESS_HEADER=True-Client-IP node build +``` + +> Headers can easily be spoofed. As with `PROTOCOL_HEADER` and `HOST_HEADER`, you should [know what you're doing](https://adam-p.ca/blog/2022/03/x-forwarded-for/) before setting these. + All of these environment variables can be changed, if necessary, using the `env` option: ```js @@ -72,6 +80,7 @@ env: { port: 'MY_PORT_VARIABLE', origin: 'MY_ORIGINURL', headers: { + address: 'MY_ADDRESS_HEADER', protocol: 'MY_PROTOCOL_HEADER', host: 'MY_HOST_HEADER' } @@ -85,9 +94,23 @@ MY_ORIGINURL=https://my.site \ node build ``` -### trustProxy +### xForwardedForIndex + +If the `ADDRESS_HEADER` is `X-Forwarded-For`, the header value will contain a comma-separated list of IP addresses. For example, if there are three proxies between your server and the client, proxy 3 will forward the addresses of the client and the first two proxies: + +``` +, , +``` + +To get the client address we could use `xForwardedFor: 0` or `xForwardedFor: -3`, which counts back from the number of addresses. + +**X-Forwarded-For is [trivial to spoof](https://adam-p.ca/blog/2022/03/x-forwarded-for/), howevever**: + +``` +, , , +``` -In order for `event.clientAddress` to show the client's IP address, `adapter-node` must read it from one of several possible request headers. Since these headers can be spoofed, it will only do this if `trustProxy` is `true`. +For that reason you should always use a negative number (depending on the number of proxies) if you need to trust `event.clientAddress`. In the above example, `0` would yield the spoofed address while `-3` would continue to work. ## Custom server diff --git a/packages/adapter-node/index.d.ts b/packages/adapter-node/index.d.ts index 92e0ec953228..4422fade6753 100644 --- a/packages/adapter-node/index.d.ts +++ b/packages/adapter-node/index.d.ts @@ -15,11 +15,12 @@ interface AdapterOptions { port?: string; origin?: string; headers?: { + address?: string; protocol?: string; host?: string; }; }; - trustProxy?: boolean; + xForwardedForIndex?: number; } declare function plugin(options?: AdapterOptions): Adapter; diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index ca8ba8ff5eb5..3ed23d87576f 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -19,11 +19,12 @@ export default function ({ port: port_env = 'PORT', origin: origin_env = 'ORIGIN', headers: { + address: address_header_env = 'ADDRESS_HEADER', protocol: protocol_header_env = 'PROTOCOL_HEADER', host: host_header_env = 'HOST_HEADER' } = {} } = {}, - trustProxy = false + xForwardedForIndex = -1 } = {}) { return { name: '@sveltejs/adapter-node', @@ -54,7 +55,8 @@ export default function ({ ORIGIN: origin_env ? `process.env[${JSON.stringify(origin_env)}]` : 'undefined', PROTOCOL_HEADER: JSON.stringify(protocol_header_env), HOST_HEADER: JSON.stringify(host_header_env), - TRUST_PROXY: JSON.stringify(trustProxy) + ADDRESS_HEADER: JSON.stringify(address_header_env), + X_FORWARDED_FOR_INDEX: JSON.stringify(xForwardedForIndex) } }); diff --git a/packages/adapter-node/src/get-client-address.js b/packages/adapter-node/src/get-client-address.js deleted file mode 100644 index 2163b4f2a620..000000000000 --- a/packages/adapter-node/src/get-client-address.js +++ /dev/null @@ -1,72 +0,0 @@ -const v4 = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; -const v6 = /^([a-f0-9]+:|:){1,8}(:|[a-f0-9]*)?/; - -/** - * Determines if a string resembles an IP address. It does _not_ check that - * the IP address is valid, since that's a much more complex task and - * it's not really necessary here - * @param {string | void} str - */ -function is_ip_like(str) { - return str ? v4.test(str) || v6.test(str) : false; -} - -/** - * Extract the client IP address from an x-forwarded-for header - * @param {string | void} str - */ -function extract_from_x_forwarded_for(str) { - if (!str) return null; - - return str - .split(', ') - .map((address) => { - // strip port, if this is an IPv4 address - const parts = address.split(':'); - if (parts.length === 2) return parts[0]; - - return address; - }) - .find(is_ip_like); -} - -const candidates = [ - 'cf-connecting-ip', - 'fastly-client-ip', - 'true-client-ip', - 'x-real-ip', - 'x-cluster-client-ip', - 'x-forwarded', - 'forwarded-for', - 'forwarded' -]; - -/** @param {import('http').IncomingMessage} req */ -export function get_client_address(req) { - const headers = /** @type {Record} */ (req.headers); - - // this follows the order of checks from https://github.com/pbojinov/request-ip/blob/master/src/index.js - - if (is_ip_like(headers['x-client-ip'])) { - return headers['x-client-ip']; - } - - { - const address = extract_from_x_forwarded_for(headers['x-forwarded-for']); - if (address) return address; - } - - for (const candidate of candidates) { - const value = headers[candidate]; - if (is_ip_like(value)) return value; - } - - return ( - req.connection?.remoteAddress || - // @ts-expect-error - req.connection?.socket?.remoteAddress || - req.socket?.remoteAddress || - // @ts-expect-error - req.info?.remoteAddress - ); -} diff --git a/packages/adapter-node/src/handler.d.ts b/packages/adapter-node/src/handler.d.ts index 5af5fec641c7..5c89cb6732f1 100644 --- a/packages/adapter-node/src/handler.d.ts +++ b/packages/adapter-node/src/handler.d.ts @@ -2,9 +2,10 @@ import type { Handle } from '@sveltejs/kit'; declare global { const ORIGIN: string; + const ADDRESS_HEADER: string; const HOST_HEADER: string; const PROTOCOL_HEADER: string; - const TRUST_PROXY: boolean; + const X_FORWARDED_FOR_INDEX: number; } export const handler: Handle; diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index ef19ef742e96..e0a4a8380b62 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -6,12 +6,13 @@ import { fileURLToPath } from 'url'; import { getRequest, setResponse } from '@sveltejs/kit/node'; import { Server } from 'SERVER'; import { manifest } from 'MANIFEST'; -import { get_client_address } from './get-client-address'; -/* global ORIGIN, PROTOCOL_HEADER, HOST_HEADER, TRUST_PROXY */ +/* global ORIGIN, ADDRESS_HEADER, PROTOCOL_HEADER, HOST_HEADER */ const server = new Server(manifest); const origin = ORIGIN; + +const address_header = ADDRESS_HEADER && (process.env[ADDRESS_HEADER] || '').toLowerCase(); const protocol_header = PROTOCOL_HEADER && process.env[PROTOCOL_HEADER]; const host_header = (HOST_HEADER && process.env[HOST_HEADER]) || 'host'; @@ -50,11 +51,25 @@ const ssr = async (req, res) => { res, await server.respond(request, { getClientAddress: () => { - if (TRUST_PROXY) { - return get_client_address(req); + if (address_header) { + const value = /** @type {string} */ (req.headers[address_header]) || ''; + + if (address_header === 'x-forwarded-for') { + const addresses = value.split(','); + return addresses[(addresses.length + X_FORWARDED_FOR_INDEX) % addresses.length].trim(); + } + + return value; } - throw new Error('You must enable the adapter-node trustProxy option to read clientAddress'); + return ( + req.connection?.remoteAddress || + // @ts-expect-error + req.connection?.socket?.remoteAddress || + req.socket?.remoteAddress || + // @ts-expect-error + req.info?.remoteAddress + ); } }) );