@@ -26,6 +26,8 @@ const debug = require("debug")("SOEServer");
2626
2727const MAX_PACKETS_PER_IP_PER_SEC = 500 ;
2828const MAX_SESSIONS_PER_IP_PER_MIN = 5 ;
29+ const MIN_UDP_LENGTH = 512 ;
30+ const MAX_UDP_LENGTH = 65535 ;
2931
3032interface RateWindow {
3133 count : number ;
@@ -279,8 +281,19 @@ export class SOEServer extends EventEmitter {
279281
280282 private _createClient ( remote : RemoteInfo ) {
281283 const client = new SOEClient ( remote , this . _crcSeed , this . _cryptoKey ) ;
284+ // Armed here rather than in the SessionRequest handler: a client that never
285+ // completes a session request must still expire. We delete it ourselves
286+ // because the gateway/zone disconnect path can only find clients by
287+ // sessionId, which a half-open client never sets.
288+ client . lastKeepAliveTimer = this . keepAliveTimeoutTime
289+ ? setTimeout ( ( ) => {
290+ debug ( "Client keep alive timeout" ) ;
291+ this . deleteClient ( client ) ;
292+ this . emit ( "disconnect" , client ) ;
293+ } , this . keepAliveTimeoutTime )
294+ : null ;
282295 // Start a persistent sending loop for this client
283- ( client as any ) . sendingInterval = setInterval ( ( ) => {
296+ client . sendingInterval = setInterval ( ( ) => {
284297 this . sendingProcess ( client ) ;
285298 } , this . _waitTimeMs ) ;
286299 client . inputStream . on ( "appdata" , ( data : Buffer ) => {
@@ -329,7 +342,12 @@ export class SOEServer extends EventEmitter {
329342 "Received session request from " + client . address + ":" + client . port
330343 ) ;
331344 client . sessionId = packet . session_id ;
332- client . clientUdpLength = packet . udp_length ;
345+ // udp_length is attacker controlled. An unclamped value makes the
346+ // derived fragment size <= 0, which hangs writeReliable() forever.
347+ client . clientUdpLength = Math . min (
348+ Math . max ( packet . udp_length >>> 0 , MIN_UDP_LENGTH ) ,
349+ MAX_UDP_LENGTH
350+ ) ;
333351 client . protocolName = packet . protocol ;
334352 client . serverUdpLength = this . _udpLength ;
335353 client . crcSeed = this . _crcSeed ;
@@ -340,14 +358,6 @@ export class SOEServer extends EventEmitter {
340358 client . outputStream . setFragmentSize (
341359 client . clientUdpLength - ( 4 + this . _crcLength )
342360 ) ;
343- // setup the keep alive timer
344- client . lastKeepAliveTimer = this . keepAliveTimeoutTime
345- ? setTimeout ( ( ) => {
346- debug ( "Client keep alive timeout" ) ;
347- this . emit ( "disconnect" , client ) ;
348- } , this . keepAliveTimeoutTime )
349- : null ;
350-
351361 const sessionReply = this . createLogicalPacket ( SoeOpcode . SessionReply , {
352362 session_id : client . sessionId ,
353363 crc_seed : client . crcSeed ,
@@ -467,37 +477,15 @@ export class SOEServer extends EventEmitter {
467477 try {
468478 if ( ! this . _checkIpRateLimit ( remote . address ) ) return ;
469479
470- let client : SOEClient ;
471480 const clientId = SOEClient . getClientId ( remote ) ;
472481 debug ( data . length + " bytes from " , clientId ) ;
473- // if doesn't know the client
474- if ( ! this . _clients . has ( clientId ) ) {
475- // if it's not a session request then we ignore it
476- if ( data [ 1 ] !== 1 ) {
477- return ;
478- }
479- if ( ! this . _checkSessionRateLimit ( remote . address ) ) return ;
480- client = this . _createClient ( remote ) ;
481- } else {
482- client = this . _clients . get ( clientId ) as SOEClient ;
483- }
484- if ( data [ 0 ] === 0x00 ) {
485- const raw_parsed_data : string = this . _protocol . parse ( data ) ;
486- if ( raw_parsed_data ) {
487- const parsed_data = JSON . parse ( raw_parsed_data ) ;
488- if ( parsed_data . name === "Error" ) {
489- debug ( "parsing error " + parsed_data . error ) ;
490- debug ( parsed_data ) ;
491- } else {
492- if ( client . lastKeepAliveTimer ) {
493- client . lastKeepAliveTimer . refresh ( ) ;
494- }
495- this . handlePacket ( client , parsed_data ) ;
496- }
497- } else {
498- debug ( "Unmanaged packet from client" , clientId , data ) ;
499- }
500- } else {
482+
483+ let client = this . _clients . get ( clientId ) ;
484+
485+ // Non-SOE data is only ever valid for an established client and must
486+ // never be able to create one.
487+ if ( data [ 0 ] !== 0x00 ) {
488+ if ( ! client ) return ;
501489 if ( this . _allowRawDataReception ) {
502490 debug ( "Raw data received from client" , clientId , data ) ;
503491 this . emit ( "appdata" , client , data , true ) ; // Unreliable + Unordered
@@ -508,10 +496,46 @@ export class SOEServer extends EventEmitter {
508496 data
509497 ) ;
510498 }
499+ return ;
511500 }
501+
502+ // Cheap pre-filter so a junk flood never reaches the protocol parser
503+ if ( ! client && data [ 1 ] !== 1 ) return ;
504+
505+ const raw_parsed_data : string = this . _protocol . parse ( data ) ;
506+ if ( ! raw_parsed_data ) {
507+ debug ( "Unmanaged packet from client" , clientId , data ) ;
508+ return ;
509+ }
510+
511+ let parsed_data ;
512+ try {
513+ parsed_data = JSON . parse ( raw_parsed_data ) ;
514+ } catch {
515+ // h1emu-core can emit malformed JSON for hostile input, drop the packet
516+ debug ( "Failed to parse packet JSON from " + clientId ) ;
517+ return ;
518+ }
519+
520+ if ( parsed_data . name === "Error" ) {
521+ debug ( "parsing error " + parsed_data . error ) ;
522+ debug ( parsed_data ) ;
523+ return ;
524+ }
525+
526+ if ( ! client ) {
527+ // An unknown client may only ever open a session, nothing else.
528+ if ( parsed_data . name !== "SessionRequest" ) return ;
529+ if ( ! this . _checkSessionRateLimit ( remote . address ) ) return ;
530+ client = this . _createClient ( remote ) ;
531+ }
532+
533+ if ( client . lastKeepAliveTimer ) {
534+ client . lastKeepAliveTimer . refresh ( ) ;
535+ }
536+ this . handlePacket ( client , parsed_data ) ;
512537 } catch ( e ) {
513538 console . log ( e ) ;
514- process . exitCode = 1 ;
515539 }
516540 }
517541
@@ -523,15 +547,17 @@ export class SOEServer extends EventEmitter {
523547 if ( udpLength !== undefined ) {
524548 this . _udpLength = udpLength ;
525549 }
526- setInterval ( ( ) => {
550+ // Swept at a cadence matching the entry TTLs, otherwise a spoofed source
551+ // flood grows these maps unbounded between sweeps.
552+ this . _packetResetInterval = setInterval ( ( ) => {
527553 const now = Date . now ( ) ;
528554 for ( const [ ip , entry ] of this . _ipRateMap ) {
529555 if ( now - entry . windowStart > 2000 ) this . _ipRateMap . delete ( ip ) ;
530556 }
531557 for ( const [ ip , entry ] of this . _sessionRequestMap ) {
532558 if ( now - entry . windowStart > 62000 ) this . _sessionRequestMap . delete ( ip ) ;
533559 }
534- } , 60000 ) ;
560+ } , 5000 ) ;
535561
536562 this . _connection . on ( "message" , ( data , remote ) => {
537563 this . onMessage ( data , remote ) ;
@@ -812,10 +838,7 @@ export class SOEServer extends EventEmitter {
812838
813839 deleteClient ( client : SOEClient ) : void {
814840 client . isDeleted = true ;
815- if ( ( client as any ) . sendingInterval ) {
816- clearInterval ( ( client as any ) . sendingInterval ) ;
817- ( client as any ) . sendingInterval = null ;
818- }
841+ client . closeTimers ( ) ;
819842 this . _clients . delete ( client . soeClientId ) ;
820843 debug ( "client connection from port : " , client . port , " deleted" ) ;
821844 }
0 commit comments