Skip to content

Commit adf7b40

Browse files
authored
fix(utils): Streamline IP capturing on incoming requests (#13272)
This PR does three things: 1. It ensures we infer the IP (in RequestData integration) from IP-related headers, if available. 2. It ensures we do not send these headers if IP capturing is not enabled (which is the default) 3. It removes the custom handling we had for this in remix, as this should now just be handled generally Closes #13260
1 parent 0ca8821 commit adf7b40

File tree

6 files changed

+387
-105
lines changed

6 files changed

+387
-105
lines changed

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

-17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable complexity */
21
// Based on Remix's implementation of Fetch API
32
// https://github.com/remix-run/web-std-io/blob/d2a003fe92096aaf97ab2a618b74875ccaadc280/packages/fetch/
43
// The MIT License (MIT)
@@ -23,10 +22,6 @@
2322
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2423
// SOFTWARE.
2524

26-
import { logger } from '@sentry/utils';
27-
28-
import { DEBUG_BUILD } from './debug-build';
29-
import { getClientIPAddress } from './vendor/getIpAddress';
3025
import type { RemixRequest } from './vendor/types';
3126

3227
/*
@@ -124,15 +119,6 @@ export const normalizeRemixRequest = (request: RemixRequest): Record<string, any
124119
headers.set('Connection', 'close');
125120
}
126121

127-
let ip;
128-
129-
// Using a try block here not to break the whole request if we can't get the IP address
130-
try {
131-
ip = getClientIPAddress(headers);
132-
} catch (e) {
133-
DEBUG_BUILD && logger.warn('Could not get client IP address', e);
134-
}
135-
136122
// HTTP-network fetch step 4.2
137123
// chunked encoding is handled by Node.js
138124
const search = getSearch(parsedURL);
@@ -156,9 +142,6 @@ export const normalizeRemixRequest = (request: RemixRequest): Record<string, any
156142

157143
// [SENTRY] For compatibility with Sentry SDK RequestData parser, adding `originalUrl` property.
158144
originalUrl: parsedURL.href,
159-
160-
// [SENTRY] Adding `ip` property if found inside headers.
161-
ip,
162145
};
163146

164147
return requestOptions;

packages/remix/test/utils/getIpAddress.test.ts

-42
This file was deleted.

packages/remix/test/utils/normalizeRemixRequest.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ describe('normalizeRemixRequest', () => {
8383
hostname: 'example.com',
8484
href: 'https://example.com/api/json?id=123',
8585
insecureHTTPParser: undefined,
86-
ip: null,
8786
method: 'GET',
8887
originalUrl: 'https://example.com/api/json?id=123',
8988
path: '/api/json?id=123',

packages/utils/src/requestdata.ts

+22-12
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { isPlainObject, isString } from './is';
1313
import { logger } from './logger';
1414
import { normalize } from './normalize';
1515
import { stripUrlQueryAndFragment } from './url';
16+
import { getClientIPAddress, ipHeaderNames } from './vendor/getIpAddress';
1617

1718
const DEFAULT_INCLUDES = {
1819
ip: false,
@@ -98,7 +99,6 @@ export function extractPathForTransaction(
9899
return [name, source];
99100
}
100101

101-
/** JSDoc */
102102
function extractTransaction(req: PolymorphicRequest, type: boolean | TransactionNamingScheme): string {
103103
switch (type) {
104104
case 'path': {
@@ -116,7 +116,6 @@ function extractTransaction(req: PolymorphicRequest, type: boolean | Transaction
116116
}
117117
}
118118

119-
/** JSDoc */
120119
function extractUserData(
121120
user: {
122121
[key: string]: unknown;
@@ -146,17 +145,16 @@ function extractUserData(
146145
*/
147146
export function extractRequestData(
148147
req: PolymorphicRequest,
149-
options?: {
148+
options: {
150149
include?: string[];
151-
},
150+
} = {},
152151
): ExtractedNodeRequestData {
153-
const { include = DEFAULT_REQUEST_INCLUDES } = options || {};
154-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
155-
const requestData: { [key: string]: any } = {};
152+
const { include = DEFAULT_REQUEST_INCLUDES } = options;
153+
const requestData: { [key: string]: unknown } = {};
156154

157155
// headers:
158156
// node, express, koa, nextjs: req.headers
159-
const headers = (req.headers || {}) as {
157+
const headers = (req.headers || {}) as typeof req.headers & {
160158
host?: string;
161159
cookie?: string;
162160
};
@@ -191,6 +189,14 @@ export function extractRequestData(
191189
delete (requestData.headers as { cookie?: string }).cookie;
192190
}
193191

192+
// Remove IP headers in case IP data should not be included in the event
193+
if (!include.includes('ip')) {
194+
ipHeaderNames.forEach(ipHeaderName => {
195+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
196+
delete (requestData.headers as Record<string, unknown>)[ipHeaderName];
197+
});
198+
}
199+
194200
break;
195201
}
196202
case 'method': {
@@ -264,9 +270,12 @@ export function addRequestDataToEvent(
264270
};
265271

266272
if (include.request) {
267-
const extractedRequestData = Array.isArray(include.request)
268-
? extractRequestData(req, { include: include.request })
269-
: extractRequestData(req);
273+
const includeRequest = Array.isArray(include.request) ? [...include.request] : [...DEFAULT_REQUEST_INCLUDES];
274+
if (include.ip) {
275+
includeRequest.push('ip');
276+
}
277+
278+
const extractedRequestData = extractRequestData(req, { include: includeRequest });
270279

271280
event.request = {
272281
...event.request,
@@ -288,8 +297,9 @@ export function addRequestDataToEvent(
288297
// client ip:
289298
// node, nextjs: req.socket.remoteAddress
290299
// express, koa: req.ip
300+
// It may also be sent by proxies as specified in X-Forwarded-For or similar headers
291301
if (include.ip) {
292-
const ip = req.ip || (req.socket && req.socket.remoteAddress);
302+
const ip = (req.headers && getClientIPAddress(req.headers)) || req.ip || (req.socket && req.socket.remoteAddress);
293303
if (ip) {
294304
event.user = {
295305
...event.user,

packages/remix/src/utils/vendor/getIpAddress.ts renamed to packages/utils/src/vendor/getIpAddress.ts

+49-33
Original file line numberDiff line numberDiff line change
@@ -23,58 +23,46 @@
2323
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2424
// SOFTWARE.
2525

26-
import { isIP } from 'net';
26+
// The headers to check, in priority order
27+
export const ipHeaderNames = [
28+
'X-Client-IP',
29+
'X-Forwarded-For',
30+
'Fly-Client-IP',
31+
'CF-Connecting-IP',
32+
'Fastly-Client-Ip',
33+
'True-Client-Ip',
34+
'X-Real-IP',
35+
'X-Cluster-Client-IP',
36+
'X-Forwarded',
37+
'Forwarded-For',
38+
'Forwarded',
39+
'X-Vercel-Forwarded-For',
40+
];
2741

2842
/**
2943
* Get the IP address of the client sending a request.
3044
*
3145
* It receives a Request headers object and use it to get the
3246
* IP address from one of the following headers in order.
3347
*
34-
* - X-Client-IP
35-
* - X-Forwarded-For
36-
* - Fly-Client-IP
37-
* - CF-Connecting-IP
38-
* - Fastly-Client-Ip
39-
* - True-Client-Ip
40-
* - X-Real-IP
41-
* - X-Cluster-Client-IP
42-
* - X-Forwarded
43-
* - Forwarded-For
44-
* - Forwarded
45-
*
4648
* If the IP address is valid, it will be returned. Otherwise, null will be
4749
* returned.
4850
*
4951
* If the header values contains more than one IP address, the first valid one
5052
* will be returned.
5153
*/
52-
export function getClientIPAddress(headers: Headers): string | null {
53-
// The headers to check, in priority order
54-
const headerNames = [
55-
'X-Client-IP',
56-
'X-Forwarded-For',
57-
'Fly-Client-IP',
58-
'CF-Connecting-IP',
59-
'Fastly-Client-Ip',
60-
'True-Client-Ip',
61-
'X-Real-IP',
62-
'X-Cluster-Client-IP',
63-
'X-Forwarded',
64-
'Forwarded-For',
65-
'Forwarded',
66-
];
67-
54+
export function getClientIPAddress(headers: { [key: string]: string | string[] | undefined }): string | null {
6855
// This will end up being Array<string | string[] | undefined | null> because of the various possible values a header
6956
// can take
70-
const headerValues = headerNames.map((headerName: string) => {
71-
const value = headers.get(headerName);
57+
const headerValues = ipHeaderNames.map((headerName: string) => {
58+
const rawValue = headers[headerName];
59+
const value = Array.isArray(rawValue) ? rawValue.join(';') : rawValue;
7260

7361
if (headerName === 'Forwarded') {
7462
return parseForwardedHeader(value);
7563
}
7664

77-
return value?.split(',').map((v: string) => v.trim());
65+
return value && value.split(',').map((v: string) => v.trim());
7866
});
7967

8068
// Flatten the array and filter out any falsy entries
@@ -92,7 +80,7 @@ export function getClientIPAddress(headers: Headers): string | null {
9280
return ipAddress || null;
9381
}
9482

95-
function parseForwardedHeader(value: string | null): string | null {
83+
function parseForwardedHeader(value: string | null | undefined): string | null {
9684
if (!value) {
9785
return null;
9886
}
@@ -105,3 +93,31 @@ function parseForwardedHeader(value: string | null): string | null {
10593

10694
return null;
10795
}
96+
97+
//
98+
/**
99+
* Custom method instead of importing this from `net` package, as this only exists in node
100+
* Accepts:
101+
* 127.0.0.1
102+
* 192.168.1.1
103+
* 192.168.1.255
104+
* 255.255.255.255
105+
* 10.1.1.1
106+
* 0.0.0.0
107+
* 2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5
108+
*
109+
* Rejects:
110+
* 1.1.1.01
111+
* 30.168.1.255.1
112+
* 127.1
113+
* 192.168.1.256
114+
* -1.2.3.4
115+
* 1.1.1.1.
116+
* 3...3
117+
* 192.168.1.099
118+
*/
119+
function isIP(str: string): boolean {
120+
const regex =
121+
/(?:^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$)|(?:^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$)/;
122+
return regex.test(str);
123+
}

0 commit comments

Comments
 (0)