Skip to content

Commit 4bb9db3

Browse files
committed
fix: only trust forwarded IP header from configured trusted proxies
1 parent ef9209c commit 4bb9db3

5 files changed

Lines changed: 71 additions & 5 deletions

File tree

CONFIGURATION.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Running `nostream` for the first time creates the settings file in `<project_roo
7373
| info.contact | Relay operator's contact. (e.g. mailto:operator@relay-your-domain.com) |
7474
| network.maxPayloadSize | Maximum number of bytes accepted per WebSocket frame |
7575
| network.remoteIpHeader | HTTP header from proxy containing IP address from client. |
76+
| network.trustedProxies | Optional allow-list of proxy IPs allowed to set `network.remoteIpHeader`; otherwise socket remote IP is used. |
7677
| payments.enabled | Enabled payments. Defaults to false. |
7778
| payments.processor | Either `zebedee`, `lnbits`, `lnurl`. |
7879
| payments.feeSchedules.admission[].enabled | Enables admission fee. Defaults to false. |
@@ -119,4 +120,4 @@ Running `nostream` for the first time creates the settings file in `<project_roo
119120
| limits.message.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
120121
| limits.admissionCheck.rateLimits[].period | Rate limit period in milliseconds. |
121122
| limits.admissionCheck.rateLimits[].rate | Maximum number of admission checks during period. |
122-
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
123+
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |

resources/default-settings.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ network:
3939
maxPayloadSize: 524288
4040
# Comment the next line if using CloudFlare proxy
4141
remoteIpHeader: x-forwarded-for
42+
# Optional: only trust forwarding headers from these proxy IPs
43+
trustedProxies: []
4244
# Uncomment the next line if using CloudFlare proxy
4345
# remoteIpHeader: cf-connecting-ip
4446
workers:

src/@types/settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface Info {
1414
export interface Network {
1515
maxPayloadSize?: number
1616
remoteIpHeader?: string
17+
trustedProxies?: string[]
1718
}
1819

1920
export interface RateLimit {

src/utils/http.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@ import { IncomingMessage } from 'http'
22

33
import { Settings } from '../@types/settings'
44

5+
const normalizeIpAddress = (input: string): string => {
6+
if (input.startsWith('::ffff:')) {
7+
return input.slice(7)
8+
}
9+
10+
return input
11+
}
12+
13+
const isTrustedProxy = (ipAddress: string, settings: Settings): boolean => {
14+
const trustedProxies = settings.network?.trustedProxies
15+
16+
if (!Array.isArray(trustedProxies) || trustedProxies.length === 0) {
17+
return false
18+
}
19+
20+
const normalizedRemote = normalizeIpAddress(ipAddress)
21+
22+
return trustedProxies.some((trustedProxy) => {
23+
return normalizeIpAddress(trustedProxy) === normalizedRemote
24+
})
25+
}
26+
527
export const getRemoteAddress = (request: IncomingMessage, settings: Settings): string => {
628
let header: string | undefined
729
// TODO: Remove deprecation warning
@@ -13,7 +35,22 @@ export const getRemoteAddress = (request: IncomingMessage, settings: Settings):
1335
header = settings.network.remoteIpHeader as string
1436
}
1537

16-
const result = (request.headers[header] ?? request.socket.remoteAddress) as string
38+
const trustedProxies = settings.network?.trustedProxies
39+
if (header && (!Array.isArray(trustedProxies) || trustedProxies.length === 0)) {
40+
console.warn('WARNING: network.remoteIpHeader is set but network.trustedProxies is empty. Forwarded headers will be ignored. Add your proxy IP to network.trustedProxies.')
41+
}
42+
43+
const headerAddress = header
44+
? request.headers[header]
45+
: undefined
46+
const socketAddress = request.socket.remoteAddress
47+
48+
const trustedProxy = typeof socketAddress === 'string'
49+
&& isTrustedProxy(socketAddress, settings)
50+
51+
const result = trustedProxy && typeof headerAddress === 'string'
52+
? headerAddress
53+
: socketAddress
1754

18-
return result.split(',')[0]
55+
return (result as string).split(',')[0].trim()
1956
}

test/unit/utils/http.spec.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('getRemoteAddress', () => {
2525
expect(
2626
getRemoteAddress(
2727
request,
28-
{ network: { 'remote_ip_header': header } } as any,
28+
{ network: { 'remote_ip_header': header, trustedProxies: [socketAddress] } } as any,
2929
)
3030
).to.equal(address)
3131
})
@@ -34,7 +34,32 @@ describe('getRemoteAddress', () => {
3434
expect(
3535
getRemoteAddress(
3636
request,
37-
{ network: { remoteIpHeader: header } } as any,
37+
{ network: { remoteIpHeader: header, trustedProxies: [socketAddress] } } as any,
38+
)
39+
).to.equal(address)
40+
})
41+
42+
it('returns socket address when proxy is not trusted', () => {
43+
expect(
44+
getRemoteAddress(
45+
request,
46+
{ network: { remoteIpHeader: header, trustedProxies: ['1.1.1.1'] } } as any,
47+
)
48+
).to.equal(socketAddress)
49+
})
50+
51+
it('normalizes ipv4-mapped trusted proxy addresses', () => {
52+
expect(
53+
getRemoteAddress(
54+
{
55+
headers: {
56+
[header]: address,
57+
},
58+
socket: {
59+
remoteAddress: '::ffff:127.0.0.1',
60+
},
61+
} as any,
62+
{ network: { remoteIpHeader: header, trustedProxies: ['127.0.0.1'] } } as any,
3863
)
3964
).to.equal(address)
4065
})

0 commit comments

Comments
 (0)