Skip to content

Commit b52336b

Browse files
fix(remix): Attempt to extract user IP from request headers. (#6263)
Remix requests don't contain client IP addresses in `req.ip` or `req.socket.*`. To extract them, we need to iterate over a set of request headers that may contain that info. I have vendored / modified an implementation of that function from third-party utility set [`remix-utils`](https://github.com/sergiodxa/remix-utils#getclientipaddress). (Using as a dependency is not possible because of incompatible TS syntax.) Co-authored-by: Katie Byers <[email protected]>
1 parent 94f4ae9 commit b52336b

File tree

2 files changed

+98
-0
lines changed

2 files changed

+98
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Vendored / modified from @sergiodxa/remix-utils
2+
// https://github.com/sergiodxa/remix-utils/blob/02af80e12829a53696bfa8f3c2363975cf59f55e/src/server/get-client-ip-address.ts
3+
4+
import { isIP } from 'net';
5+
6+
/**
7+
* Get the IP address of the client sending a request.
8+
*
9+
* It receives a Request headers object and use it to get the
10+
* IP address from one of the following headers in order.
11+
*
12+
* - X-Client-IP
13+
* - X-Forwarded-For
14+
* - Fly-Client-IP
15+
* - CF-Connecting-IP
16+
* - Fastly-Client-Ip
17+
* - True-Client-Ip
18+
* - X-Real-IP
19+
* - X-Cluster-Client-IP
20+
* - X-Forwarded
21+
* - Forwarded-For
22+
* - Forwarded
23+
*
24+
* If the IP address is valid, it will be returned. Otherwise, null will be
25+
* returned.
26+
*
27+
* If the header values contains more than one IP address, the first valid one
28+
* will be returned.
29+
*/
30+
export function getClientIPAddress(headers: Headers): string | null {
31+
// The headers to check, in priority order
32+
const headerNames = [
33+
'X-Client-IP',
34+
'X-Forwarded-For',
35+
'Fly-Client-IP',
36+
'CF-Connecting-IP',
37+
'Fastly-Client-Ip',
38+
'True-Client-Ip',
39+
'X-Real-IP',
40+
'X-Cluster-Client-IP',
41+
'X-Forwarded',
42+
'Forwarded-For',
43+
'Forwarded',
44+
];
45+
46+
// This will end up being Array<string | string[] | undefined | null> because of the various possible values a header
47+
// can take
48+
const headerValues = headerNames.map((headerName: string) => {
49+
const value = headers.get(headerName);
50+
51+
if (headerName === 'Forwarded') {
52+
return parseForwardedHeader(value);
53+
}
54+
55+
return value?.split(', ');
56+
});
57+
58+
// Flatten the array and filter out any falsy entries
59+
const flattenedHeaderValues = headerValues.reduce((acc: string[], val) => {
60+
if (!val) {
61+
return acc;
62+
}
63+
64+
return acc.concat(val);
65+
}, []);
66+
67+
// Find the first value which is a valid IP address, if any
68+
const ipAddress = flattenedHeaderValues.find(ip => ip !== null && isIP(ip));
69+
70+
return ipAddress || null;
71+
}
72+
73+
function parseForwardedHeader(value: string | null): string | null {
74+
if (!value) {
75+
return null;
76+
}
77+
78+
for (const part of value.split(';')) {
79+
if (part.startsWith('for=')) {
80+
return part.slice(4);
81+
}
82+
}
83+
84+
return null;
85+
}

packages/remix/src/utils/web-fetch.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Based on Remix's implementation of Fetch API
22
// https://github.com/remix-run/web-std-io/tree/main/packages/fetch
33

4+
import { getClientIPAddress } from './getIpAddress';
45
import { RemixRequest } from './types';
56

67
/*
@@ -92,6 +93,15 @@ export const normalizeRemixRequest = (request: RemixRequest): Record<string, any
9293
headers.set('Connection', 'close');
9394
}
9495

96+
let ip;
97+
98+
// Using a try block here just to stay on the safe side
99+
try {
100+
ip = getClientIPAddress(headers);
101+
} catch (e) {
102+
// ignore
103+
}
104+
95105
// HTTP-network fetch step 4.2
96106
// chunked encoding is handled by Node.js
97107
const search = getSearch(parsedURL);
@@ -116,6 +126,9 @@ export const normalizeRemixRequest = (request: RemixRequest): Record<string, any
116126

117127
// [SENTRY] For compatibility with Sentry SDK RequestData parser, adding `originalUrl` property.
118128
originalUrl: parsedURL.href,
129+
130+
// [SENTRY] Adding `ip` property if found inside headers.
131+
ip,
119132
};
120133

121134
return requestOptions;

0 commit comments

Comments
 (0)