Skip to content

Commit 2644e0f

Browse files
authored
Merge branch 'main' into test/nip02-contact-list-integration
2 parents 35a466c + 5bf1a58 commit 2644e0f

6 files changed

Lines changed: 139 additions & 13 deletions

File tree

.changeset/tasty-parents-pump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
Fix IP spoofing via unconditional trust of x-forwarded-for header

CONFIGURATION.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,12 @@ The settings below are listed in alphabetical order by name. Please keep this ta
129129
| limits.event.content[].kinds | List of event kinds to apply limit. Use `[min, max]` for ranges. Optional. |
130130
| limits.event.content[].maxLength | Maximum length of `content`. Defaults to 1 MB. Disabled when set to zero. |
131131
| limits.event.createdAt.maxPositiveDelta | Maximum number of seconds an event's `created_at` can be in the future. Defaults to 900 (15 minutes). Disabled when set to zero. |
132-
| limits.event.createdAt.minNegativeDelta | Maximum number of secodns an event's `created_at` can be in the past. Defaults to zero. Disabled when set to zero. |
133-
| limits.event.eventId.minLeadingZeroBits | Leading zero bits required on every incoming event for proof of work. |
134-
| | Defaults to zero. Disabled when set to zero. |
132+
| limits.event.createdAt.minNegativeDelta | Maximum number of seconds an event's `created_at` can be in the past. Defaults to zero. Disabled when set to zero. |
133+
| limits.event.eventId.minLeadingZeroBits | Leading zero bits required on every incoming event for proof of work. Defaults to zero. Disabled when set to zero. |
135134
| limits.event.kind.blacklist | List of event kinds to always reject. Leave empty to allow any. |
136135
| limits.event.kind.whitelist | List of event kinds to always allow. Leave empty to allow any. |
137136
| limits.event.pubkey.blacklist | List of public keys to always reject. Public keys in this list will not be able to post to this relay. |
138-
| limits.event.pubkey.minLeadingZeroBits | Leading zero bits required on the public key of incoming events for proof of work. |
139-
| | Defaults to zero. Disabled when set to zero. |
137+
| limits.event.pubkey.minLeadingZeroBits | Leading zero bits required on the public key of incoming events for proof of work. Defaults to zero. Disabled when set to zero. |
140138
| limits.event.pubkey.whitelist | List of public keys to always allow. Only public keys in this list will be able to post to this relay. Use for private relays. |
141139
| limits.event.rateLimits[].kinds | List of event kinds rate limited. Use `[min, max]` for ranges. Optional. |
142140
| limits.event.rateLimits[].period | Rate limiting period in milliseconds. For `sliding_window`: the time window during which requests are counted. For `ewma`: the half-life of the exponential decay — shorter values forget bursts faster, longer values are stricter on bursty clients. |
@@ -184,3 +182,30 @@ The settings below are listed in alphabetical order by name. Please keep this ta
184182
| limits.admissionCheck.rateLimits[].rate | Maximum number of admission checks during period. |
185183
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
186184
| limits.rateLimiter.strategy | Rate limiting strategy. Either `ewma` or `sliding_window`. Defaults to `ewma`. When using `ewma`, the `period` field in each rate limit serves as the half-life for the exponential decay function. Note: when switching from `sliding_window` to `ewma`, consider increasing `rate` values slightly as EWMA penalizes bursty behavior more aggressively. |
185+
| mirroring.static[].address | Address of mirrored relay. (e.g. ws://100.100.100.100:8008) |
186+
| mirroring.static[].filters | Subscription filters used to mirror. |
187+
| mirroring.static[].limits.event | Event limit overrides for this mirror. See configurations under limits.event. |
188+
| mirroring.static[].secret | Secret to pass to relays. Nostream relays only. Optional. |
189+
| mirroring.static[].skipAdmissionCheck | Disable the admission fee check for events coming from this mirror. |
190+
| network.maxPayloadSize | Maximum number of bytes accepted per WebSocket frame |
191+
| network.remoteIpHeader | HTTP header from proxy containing IP address from client. |
192+
| network.trustedProxies | Optional allow-list of proxy IPs allowed to set `network.remoteIpHeader`; otherwise socket remote IP is used. |
193+
| nip05.domainBlacklist | List of domains blocked from NIP-05 verification. Authors with NIP-05 at these domains will be rejected. |
194+
| nip05.domainWhitelist | List of domains allowed for NIP-05 verification. If set, only authors verified at these domains can publish. |
195+
| nip05.maxConsecutiveFailures | Number of consecutive verification failures before giving up on an author. Defaults to 20. |
196+
| nip05.mode | NIP-05 verification mode: `enabled` requires verification, `passive` verifies without blocking, `disabled` does nothing. Defaults to `disabled`. |
197+
| nip05.verifyExpiration | Time in milliseconds before a successful NIP-05 verification expires and needs re-checking. Defaults to 604800000 (1 week). |
198+
| nip05.verifyUpdateFrequency | Minimum interval in milliseconds between re-verification attempts for a given author. Defaults to 86400000 (24 hours). |
199+
| paymentProcessors.lnbits.baseURL | Base URL of your Lnbits instance. |
200+
| paymentProcessors.lnbits.callbackBaseURL | Public-facing Nostream's Lnbits Callback URL. (e.g. https://relay.your-domain.com/callbacks/lnbits) |
201+
| paymentProcessors.lnurl.invoiceURL | [LUD-06 Pay Request](https://github.com/lnurl/luds/blob/luds/06.md) provider URL. (e.g. https://getalby.com/lnurlp/your-username) |
202+
| paymentProcessors.zebedee.baseURL | Zebedee's API base URL. |
203+
| paymentProcessors.zebedee.callbackBaseURL | Public-facing Nostream's Zebedee Callback URL (e.g. https://relay.your-domain.com/callbacks/zebedee) |
204+
| paymentProcessors.zebedee.ipWhitelist | List with Zebedee's API Production IPs. See [ZBD API Documentation](https://api-reference.zebedee.io/#c7e18276-6935-4cca-89ae-ad949efe9a6a) for more info. |
205+
| payments.enabled | Enabled payments. Defaults to false. |
206+
| payments.feeSchedules.admission[].amount | Admission fee amount in msats. |
207+
| payments.feeSchedules.admission[].enabled | Enables admission fee. Defaults to false. |
208+
| payments.feeSchedules.admission[].whitelists.event_kinds | List of event kinds to waive admission fee. Use `[min, max]` for ranges. |
209+
| payments.feeSchedules.admission[].whitelists.pubkeys | List of pubkeys to waive admission fee. |
210+
| payments.processor | Either `zebedee`, `lnbits`, `lnurl`. |
211+
| workers.count | Number of workers to spin up to handle incoming connections. Spin workers as many CPUs are available when set to zero. Defaults to zero. |

resources/default-settings.yaml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,16 @@ nip05:
5555
domainBlacklist: []
5656
network:
5757
maxPayloadSize: 524288
58-
# Comment the next line if using CloudFlare proxy
59-
remoteIpHeader: x-forwarded-for
60-
# Uncomment the next line if using CloudFlare proxy
58+
# Uncomment only when using a trusted reverse proxy and configuring trustedProxies.
59+
# remoteIpHeader: x-forwarded-for
6160
# remoteIpHeader: cf-connecting-ip
61+
# Proxy IPs allowed to set remoteIpHeader (loopback and common docker internal)
62+
trustedProxies:
63+
- "127.0.0.1"
64+
- "::ffff:127.0.0.1"
65+
- "::1"
66+
- "10.10.10.1"
67+
- "::ffff:10.10.10.1"
6268
workers:
6369
count: 0
6470
mirroring:

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: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ import { Settings } from '../@types/settings'
55

66
const logger = createLogger('http-utils')
77

8+
const normalizeIpAddress = (input: string): string => {
9+
if (input.startsWith('::ffff:')) {
10+
return input.slice(7)
11+
}
12+
13+
return input
14+
}
15+
16+
const isTrustedProxy = (ipAddress: string, settings: Settings): boolean => {
17+
const trustedProxies = settings.network?.trustedProxies
18+
19+
if (!Array.isArray(trustedProxies) || trustedProxies.length === 0) {
20+
return false
21+
}
22+
23+
const normalizedRemote = normalizeIpAddress(ipAddress)
24+
25+
return trustedProxies.some((trustedProxy) => {
26+
return normalizeIpAddress(trustedProxy) === normalizedRemote
27+
})
28+
}
29+
830
export const getRemoteAddress = (request: IncomingMessage, settings: Settings): string => {
931
let header: string | undefined
1032
// TODO: Remove deprecation warning
@@ -16,7 +38,21 @@ export const getRemoteAddress = (request: IncomingMessage, settings: Settings):
1638
header = settings.network.remoteIpHeader as string
1739
}
1840

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

21-
return result.split(',')[0]
57+
return (result as string).split(',')[0].trim()
2258
}

test/unit/utils/http.spec.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,67 @@ describe('getRemoteAddress', () => {
2222
})
2323

2424
it('returns address using network.remote_ip_address when set', () => {
25-
expect(getRemoteAddress(request, { network: { remote_ip_header: header } } as any)).to.equal(address)
25+
expect(
26+
getRemoteAddress(
27+
request,
28+
{ network: { 'remote_ip_header': header, trustedProxies: [socketAddress] } } as any,
29+
)
30+
).to.equal(address)
2631
})
2732

2833
it('returns address using network.remoteIpAddress when set', () => {
29-
expect(getRemoteAddress(request, { network: { remoteIpHeader: header } } as any)).to.equal(address)
34+
expect(
35+
getRemoteAddress(
36+
request,
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,
63+
)
64+
).to.equal(address)
3065
})
3166

3267
it('returns address from socket when header is unset', () => {
33-
expect(getRemoteAddress(request, { network: {} } as any)).to.equal(socketAddress)
68+
expect(
69+
getRemoteAddress(
70+
request,
71+
{ network: { } } as any,
72+
)
73+
).to.equal(socketAddress)
74+
})
75+
76+
it('returns first address when forwarded header is an array', () => {
77+
const arrayRequest = {
78+
headers: { [header]: [address, 'other-address'] },
79+
socket: { remoteAddress: socketAddress },
80+
} as any
81+
expect(
82+
getRemoteAddress(
83+
arrayRequest,
84+
{ network: { remoteIpHeader: header, trustedProxies: [socketAddress] } } as any,
85+
)
86+
).to.equal(address)
3487
})
3588
})

0 commit comments

Comments
 (0)