@@ -27,10 +27,11 @@ import packageJson from '../package.json' with { type: 'json' }
2727const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) )
2828const app = express ( )
2929const 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 .
3435const TRUST_PROXY_HOPS = 1
3536app . set ( 'trust proxy' , TRUST_PROXY_HOPS )
3637
@@ -567,7 +568,12 @@ app.use((_req, res) => {
567568const 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
572578server . 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
628621wss . on ( 'connection' , async ( ws , request ) => {
0 commit comments