Skip to content

Commit 7d1059e

Browse files
committed
fix: harden websocket limits
1 parent 958945e commit 7d1059e

3 files changed

Lines changed: 17 additions & 22 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ WEBSOCKET_NETWORKS=mainnet
1515
BLOCKCHAIN_POLL_INTERVAL=10000
1616
WEBSOCKET_HEARTBEAT_INTERVAL=30000
1717
WEBSOCKET_MAX_CONNECTIONS_PER_IP=5
18+
WEBSOCKET_MAX_PAYLOAD_BYTES=65536
1819

1920
# App Configuration
2021
# Public origin allowed by the API CORS allowlist (server-side). Defaults to

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ WEBSOCKET_NETWORKS=mainnet # comma-separated; add testnet if you run a
6565
BLOCKCHAIN_POLL_INTERVAL=10000
6666
WEBSOCKET_HEARTBEAT_INTERVAL=30000
6767
WEBSOCKET_MAX_CONNECTIONS_PER_IP=5
68+
WEBSOCKET_MAX_PAYLOAD_BYTES=65536
6869

6970
# Public origin allowed by the API CORS allowlist
7071
PUBLIC_BASE_URL=https://explorer.fairco.in

server/index.ts

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ import packageJson from '../package.json' with { type: 'json' }
2727
const __dirname = path.dirname(fileURLToPath(import.meta.url))
2828
const app = express()
2929
const PORT = parseInt(process.env.PORT || '8080', 10)
30+
const WEBSOCKET_MAX_PAYLOAD_BYTES = parseInt(process.env.WEBSOCKET_MAX_PAYLOAD_BYTES || '65536', 10)
3031

3132
// Behind one reverse proxy (e.g. nginx/DO app platform): trust a single hop so
3233
// `req.ip` and the rate limiter use the real client IP, not a spoofable header.
33-
// The same hop count is used to resolve the WebSocket client IP (see resolveClientIp).
34+
// WebSocket connection limits intentionally use the TCP peer address instead.
3435
const TRUST_PROXY_HOPS = 1
3536
app.set('trust proxy', TRUST_PROXY_HOPS)
3637

@@ -567,7 +568,12 @@ app.use((_req, res) => {
567568
const server = createServer(app)
568569

569570
// WebSocket setup
570-
const wss = new WebSocketServer({ noServer: true })
571+
const wss = new WebSocketServer({
572+
noServer: true,
573+
maxPayload: Number.isFinite(WEBSOCKET_MAX_PAYLOAD_BYTES) && WEBSOCKET_MAX_PAYLOAD_BYTES > 0
574+
? WEBSOCKET_MAX_PAYLOAD_BYTES
575+
: 65536,
576+
})
571577

572578
server.on('upgrade', (request, socket, head) => {
573579
const { pathname } = parse(request.url || '', true)
@@ -602,27 +608,14 @@ function loadWsHandler(): Promise<WebSocketHandlerModule | null> {
602608
/**
603609
* Resolve the client IP for the per-IP WebSocket connection cap.
604610
*
605-
* A raw client can put any value in `X-Forwarded-For`, so trusting its left-most
606-
* entry lets attackers bypass the per-IP limit. With one trusted proxy
607-
* (`trust proxy = 1`), the authoritative client address is the entry our proxy
608-
* appended — the right-most XFF value — falling back to the real TCP peer
609-
* address (`socket.remoteAddress`) when there is no proxy header.
611+
* The WebSocket upgrade request can include a user-controlled
612+
* `X-Forwarded-For` header, especially when the service is reachable directly
613+
* or through a proxy that passes the header through. Key the unauthenticated
614+
* connection cap by the TCP peer address instead of any forwarded header so a
615+
* client cannot mint separate buckets by spoofing headers.
610616
*/
611-
function resolveClientIp(request: { headers: NodeJS.Dict<string | string[]>; socket: { remoteAddress?: string } }): string | undefined {
612-
const socketAddress = request.socket.remoteAddress
613-
if (!TRUST_PROXY_HOPS) {
614-
return socketAddress
615-
}
616-
const forwarded = request.headers['x-forwarded-for']
617-
const raw = Array.isArray(forwarded) ? forwarded.join(',') : forwarded
618-
if (raw) {
619-
const parts = raw.split(',').map(part => part.trim()).filter(Boolean)
620-
const trusted = parts[parts.length - 1]
621-
if (trusted) {
622-
return trusted
623-
}
624-
}
625-
return socketAddress
617+
function resolveClientIp(request: { socket: { remoteAddress?: string } }): string | undefined {
618+
return request.socket.remoteAddress
626619
}
627620

628621
wss.on('connection', async (ws, request) => {

0 commit comments

Comments
 (0)