securenow 7.6.8 → 7.7.0

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.
package/resolve-ip.js CHANGED
@@ -2,75 +2,150 @@
2
2
 
3
3
  const os = require('os');
4
4
  const appConfig = require('./app-config');
5
-
6
- const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
7
- const PRIVATE_IP_RE = /^(127\.|::1$|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|f[cd][0-9a-f]{2}:)/;
8
-
5
+
6
+ const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
7
+ const PRIVATE_IP_RE = /^(127\.|::1$|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|f[cd][0-9a-f]{2}:)/i;
8
+
9
9
  const trustedProxies = appConfig.listEnv('SECURENOW_TRUSTED_PROXIES');
10
- const trustedProxySet = trustedProxies.length ? new Set(trustedProxies) : null;
11
-
12
- let _hostIp = null;
13
- function getHostIp() {
14
- if (_hostIp !== null) return _hostIp;
15
- try {
16
- const ifaces = os.networkInterfaces();
17
- for (const name of Object.keys(ifaces)) {
18
- for (const iface of ifaces[name]) {
19
- if (!iface.internal && iface.family === 'IPv4') { _hostIp = iface.address; return _hostIp; }
20
- }
21
- }
22
- } catch (_) {}
23
- _hostIp = '';
24
- return _hostIp;
25
- }
26
-
27
- function isFromTrustedProxy(socketIp) {
28
- if (!socketIp) return false;
29
- const normalized = socketIp.replace(/^::ffff:/, '');
30
- if (trustedProxySet && trustedProxySet.has(normalized)) return true;
31
- return PRIVATE_IP_RE.test(socketIp);
32
- }
10
+ const trustedProxySet = trustedProxies.length ? new Set(trustedProxies.map(normalizeIp).filter(Boolean)) : null;
11
+
12
+ let _hostIps = null;
13
+ function normalizeIp(value) {
14
+ if (Array.isArray(value)) value = value[0];
15
+ if (!value) return '';
16
+ let ip = String(value).trim();
17
+ if (!ip) return '';
18
+ ip = ip.replace(/^::ffff:/i, '');
19
+ if (ip.startsWith('[')) {
20
+ const end = ip.indexOf(']');
21
+ if (end !== -1) ip = ip.slice(1, end);
22
+ } else if (/^\d{1,3}(?:\.\d{1,3}){3}:\d+$/.test(ip)) {
23
+ ip = ip.replace(/:\d+$/, '');
24
+ }
25
+ return ip;
26
+ }
27
+
28
+ function getHostIps() {
29
+ if (_hostIps !== null) return _hostIps;
30
+ const ips = new Set();
31
+ try {
32
+ const ifaces = os.networkInterfaces();
33
+ for (const name of Object.keys(ifaces)) {
34
+ for (const iface of ifaces[name]) {
35
+ if (!iface.internal && iface.address) {
36
+ const normalized = normalizeIp(iface.address);
37
+ if (normalized) ips.add(normalized);
38
+ }
39
+ }
40
+ }
41
+ } catch (_) {}
42
+ _hostIps = ips;
43
+ return _hostIps;
44
+ }
45
+
46
+ function isFromTrustedProxy(socketIp) {
47
+ if (!socketIp) return false;
48
+ const normalized = normalizeIp(socketIp);
49
+ if (!normalized) return false;
50
+ if (trustedProxySet && trustedProxySet.has(normalized)) return true;
51
+ if (getHostIps().has(normalized)) return true;
52
+ return PRIVATE_IP_RE.test(normalized);
53
+ }
54
+
55
+ function firstHeaderValue(value) {
56
+ if (Array.isArray(value)) return value.find(Boolean) || '';
57
+ return value || '';
58
+ }
59
+
60
+ function splitForwardedChain(value) {
61
+ return String(firstHeaderValue(value) || '')
62
+ .split(',')
63
+ .map((s) => normalizeIp(s))
64
+ .filter(Boolean);
65
+ }
66
+
67
+ function getHeader(headers, name) {
68
+ if (!headers) return '';
69
+ return firstHeaderValue(headers[name] || headers[name.toLowerCase()] || headers[name.toUpperCase()]);
70
+ }
71
+
72
+ function resolveClientIpWithDetails(request) {
73
+ const socketIp = normalizeIp(request.socket?.remoteAddress || '');
74
+ const headers = request.headers || {};
75
+ const trustedProxy = isFromTrustedProxy(socketIp);
76
+
77
+ const details = {
78
+ ip: socketIp,
79
+ source: socketIp ? 'socket' : 'unknown',
80
+ socketIp,
81
+ trustedProxy,
82
+ forwardedFor: String(getHeader(headers, 'x-forwarded-for') || ''),
83
+ realIp: String(getHeader(headers, 'x-real-ip') || ''),
84
+ cfConnectingIp: String(getHeader(headers, 'cf-connecting-ip') || ''),
85
+ trueClientIp: String(getHeader(headers, 'true-client-ip') || ''),
86
+ clientIp: String(getHeader(headers, 'x-client-ip') || ''),
87
+ };
88
+
89
+ if (!trustedProxy) return details;
90
+
91
+ const chain = splitForwardedChain(details.forwardedFor);
92
+ if (chain.length) {
93
+ for (let i = chain.length - 1; i >= 0; i--) {
94
+ if (!isFromTrustedProxy(chain[i])) {
95
+ return { ...details, ip: chain[i], source: 'x-forwarded-for' };
96
+ }
97
+ }
98
+ return details;
99
+ }
100
+
101
+ const headerCandidates = [
102
+ ['cf-connecting-ip', details.cfConnectingIp],
103
+ ['true-client-ip', details.trueClientIp],
104
+ ['x-real-ip', details.realIp],
105
+ ['x-client-ip', details.clientIp],
106
+ ];
107
+ for (const [source, raw] of headerCandidates) {
108
+ const ip = normalizeIp(raw);
109
+ if (ip) return { ...details, ip, source };
110
+ }
111
+
112
+ if (LOOPBACK_RE.test(socketIp)) {
113
+ const hostIp = [...getHostIps()][0] || '';
114
+ if (hostIp) return { ...details, ip: hostIp, source: 'host-network' };
115
+ }
116
+
117
+ if (trustedProxy && getHostIps().has(socketIp)) {
118
+ return { ...details, source: 'trusted-proxy-socket' };
119
+ }
120
+
121
+ return details;
122
+ }
33
123
 
34
124
  /**
35
125
  * Resolve the real client IP from an HTTP request, respecting trusted proxies.
36
126
  * Reads X-Forwarded-For / X-Real-IP only when the direct connection comes
37
127
  * from a private/trusted proxy IP. Prevents client-side IP spoofing.
38
128
  */
39
- function resolveClientIp(request) {
40
- const socketIp = request.socket?.remoteAddress || '';
41
- if (!isFromTrustedProxy(socketIp)) return socketIp;
42
-
43
- const fwd = request.headers['x-forwarded-for'];
44
- if (fwd) {
45
- const chain = String(fwd).split(',').map(s => s.trim()).filter(Boolean);
46
- for (let i = chain.length - 1; i >= 0; i--) {
47
- if (!isFromTrustedProxy(chain[i])) return chain[i];
48
- }
49
- return socketIp;
50
- }
51
- const headerIp = request.headers['x-real-ip'];
52
- if (headerIp) return headerIp;
53
-
54
- if (LOOPBACK_RE.test(socketIp)) {
55
- const hostIp = getHostIp();
56
- if (hostIp) return hostIp;
57
- }
58
- return socketIp;
59
- }
129
+ function resolveClientIp(request) {
130
+ return resolveClientIpWithDetails(request).ip || '';
131
+ }
60
132
 
61
133
  /**
62
134
  * Resolve IP from a raw TCP socket (no HTTP headers available).
63
135
  * Normalizes IPv6-mapped IPv4 addresses.
64
136
  */
65
- function resolveSocketIp(socket) {
66
- const raw = socket?.remoteAddress || '';
67
- return raw.replace(/^::ffff:/, '');
68
- }
69
-
70
- module.exports = {
71
- resolveClientIp,
72
- resolveSocketIp,
73
- isFromTrustedProxy,
74
- LOOPBACK_RE,
75
- PRIVATE_IP_RE,
76
- };
137
+ function resolveSocketIp(socket) {
138
+ const raw = socket?.remoteAddress || '';
139
+ return normalizeIp(raw);
140
+ }
141
+
142
+ module.exports = {
143
+ resolveClientIp,
144
+ resolveClientIpWithDetails,
145
+ resolveSocketIp,
146
+ isFromTrustedProxy,
147
+ normalizeIp,
148
+ getHostIps,
149
+ LOOPBACK_RE,
150
+ PRIVATE_IP_RE,
151
+ };
package/tracing.js CHANGED
@@ -343,16 +343,37 @@ const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveField
343
343
  const captureMultipart = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_MULTIPART') ?? ''));
344
344
 
345
345
  // -------- Trusted proxy IP resolution --------
346
- const { resolveClientIp, isFromTrustedProxy, LOOPBACK_RE } = require('./resolve-ip');
346
+ const { resolveClientIpWithDetails } = require('./resolve-ip');
347
347
 
348
348
  // Configure HTTP instrumentation with body capture
349
349
  const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
350
350
  const httpInstrumentation = new HttpInstrumentation({
351
351
  requestHook: (span, request) => {
352
352
  try {
353
- const clientIp = resolveClientIp(request);
354
- if (clientIp) {
355
- span.setAttribute('http.client_ip', clientIp);
353
+ const ipDetails = resolveClientIpWithDetails(request);
354
+ if (ipDetails.ip) {
355
+ span.setAttribute('http.client_ip', ipDetails.ip);
356
+ span.setAttribute('http.client_ip.source', ipDetails.source);
357
+ span.setAttribute('http.socket_ip', ipDetails.socketIp || '');
358
+ span.setAttribute('http.proxy.trusted', String(!!ipDetails.trustedProxy));
359
+ if (ipDetails.forwardedFor) {
360
+ span.setAttribute('http.forwarded_for', ipDetails.forwardedFor);
361
+ span.setAttribute('http.request.header.x_forwarded_for', ipDetails.forwardedFor);
362
+ }
363
+ if (ipDetails.realIp) {
364
+ span.setAttribute('http.real_ip', ipDetails.realIp);
365
+ span.setAttribute('http.request.header.x_real_ip', ipDetails.realIp);
366
+ }
367
+ if (ipDetails.cfConnectingIp) {
368
+ span.setAttribute('http.cf.connecting_ip', ipDetails.cfConnectingIp);
369
+ span.setAttribute('http.request.header.cf_connecting_ip', ipDetails.cfConnectingIp);
370
+ }
371
+ if (ipDetails.trueClientIp) {
372
+ span.setAttribute('http.request.header.true_client_ip', ipDetails.trueClientIp);
373
+ }
374
+ if (ipDetails.clientIp) {
375
+ span.setAttribute('http.request.header.x_client_ip', ipDetails.clientIp);
376
+ }
356
377
  }
357
378
 
358
379
  if (request.headers) {