@@ -11,14 +11,157 @@ export function setNet(netImpl) {
1111 net = netImpl ;
1212}
1313
14- export function createWebHandler ( { myHostName, localHost, port, mqConn } ) {
14+ // Security defaults
15+ const DEFAULT_MAX_MESSAGE_SIZE = 10 * 1024 * 1024 ; // 10MB
16+ const DEFAULT_MAX_CONCURRENT_SOCKETS = 100 ;
17+ const DEFAULT_RATE_LIMIT_WINDOW_MS = 1000 ; // 1 second
18+ const DEFAULT_RATE_LIMIT_MAX_REQUESTS = 100 ; // per window
19+
20+ /**
21+ * Validates that a message looks like a valid HTTP request.
22+ * This is a basic sanity check for the HTTP proxy use case.
23+ * @param {Buffer } message - The message to validate
24+ * @returns {{ valid: boolean, reason?: string } }
25+ */
26+ export function validateHttpRequest ( message ) {
27+ if ( ! Buffer . isBuffer ( message ) ) {
28+ return { valid : false , reason : 'Message must be a Buffer' } ;
29+ }
30+
31+ if ( message . length === 0 ) {
32+ return { valid : false , reason : 'Empty message' } ;
33+ }
34+
35+ // For HTTP requests, check for valid method at start
36+ const firstLine = message
37+ . slice ( 0 , Math . min ( message . length , 200 ) )
38+ . toString ( 'utf8' )
39+ . split ( '\r\n' ) [ 0 ] ;
40+
41+ // Valid HTTP methods
42+ const httpMethods = [
43+ 'GET' ,
44+ 'POST' ,
45+ 'PUT' ,
46+ 'DELETE' ,
47+ 'PATCH' ,
48+ 'HEAD' ,
49+ 'OPTIONS' ,
50+ 'CONNECT' ,
51+ 'TRACE' ,
52+ ] ;
53+ const startsWithMethod = httpMethods . some ( ( method ) => firstLine . startsWith ( method + ' ' ) ) ;
54+
55+ if ( ! startsWithMethod ) {
56+ // Could be a continuation of a previous request (body data)
57+ // or websocket frame, which is valid for existing sockets
58+ return { valid : true , isInitialRequest : false } ;
59+ }
60+
61+ // Check for HTTP version
62+ if ( ! firstLine . includes ( 'HTTP/' ) ) {
63+ return { valid : false , reason : 'Invalid HTTP request line' } ;
64+ }
65+
66+ return { valid : true , isInitialRequest : true } ;
67+ }
68+
69+ /**
70+ * Simple rate limiter using sliding window
71+ */
72+ class RateLimiter {
73+ constructor ( windowMs , maxRequests ) {
74+ this . windowMs = windowMs ;
75+ this . maxRequests = maxRequests ;
76+ this . requests = new Map ( ) ; // socketId -> [timestamps]
77+ }
78+
79+ isAllowed ( socketId ) {
80+ const now = Date . now ( ) ;
81+ const windowStart = now - this . windowMs ;
82+
83+ let timestamps = this . requests . get ( socketId ) || [ ] ;
84+
85+ // Remove old timestamps outside window
86+ timestamps = timestamps . filter ( ( ts ) => ts > windowStart ) ;
87+
88+ if ( timestamps . length >= this . maxRequests ) {
89+ this . requests . set ( socketId , timestamps ) ;
90+ return false ;
91+ }
92+
93+ timestamps . push ( now ) ;
94+ this . requests . set ( socketId , timestamps ) ;
95+ return true ;
96+ }
97+
98+ cleanup ( ) {
99+ const now = Date . now ( ) ;
100+ const windowStart = now - this . windowMs ;
101+
102+ for ( const [ socketId , timestamps ] of this . requests . entries ( ) ) {
103+ const valid = timestamps . filter ( ( ts ) => ts > windowStart ) ;
104+ if ( valid . length === 0 ) {
105+ this . requests . delete ( socketId ) ;
106+ } else {
107+ this . requests . set ( socketId , valid ) ;
108+ }
109+ }
110+ }
111+ }
112+
113+ export function createWebHandler ( {
114+ myHostName,
115+ localHost,
116+ port,
117+ mqConn,
118+ // Security options
119+ maxMessageSize = DEFAULT_MAX_MESSAGE_SIZE ,
120+ maxConcurrentSockets = DEFAULT_MAX_CONCURRENT_SOCKETS ,
121+ validateRequests = true ,
122+ enableRateLimiting = true ,
123+ rateLimitWindowMs = DEFAULT_RATE_LIMIT_WINDOW_MS ,
124+ rateLimitMaxRequests = DEFAULT_RATE_LIMIT_MAX_REQUESTS ,
125+ } ) {
15126 const sockets = { } ;
127+ const rateLimiter = enableRateLimiting
128+ ? new RateLimiter ( rateLimitWindowMs , rateLimitMaxRequests )
129+ : null ;
130+
131+ // Periodic cleanup of rate limiter state
132+ let cleanupInterval ;
133+ if ( rateLimiter ) {
134+ cleanupInterval = setInterval ( ( ) => rateLimiter . cleanup ( ) , 60000 ) ;
135+ // Don't block process exit
136+ if ( cleanupInterval . unref ) {
137+ cleanupInterval . unref ( ) ;
138+ }
139+ }
16140
17141 function handleWebRequest ( hostName , socketId , action , message ) {
142+ // Security: Validate hostname matches
18143 if ( hostName !== myHostName ) {
19144 return ; // why did this get sent to me?
20145 }
21146
147+ // Security: Validate socketId format (should be alphanumeric/dash)
148+ if ( ! socketId || ! / ^ [ \w - ] + $ / . test ( socketId ) ) {
149+ debugError ( 'Invalid socketId format:' , socketId ) ;
150+ return ;
151+ }
152+
153+ // Security: Check rate limiting
154+ if ( rateLimiter && ! rateLimiter . isAllowed ( socketId ) ) {
155+ debugError ( 'Rate limit exceeded for socket:' , socketId ) ;
156+ return ;
157+ }
158+
159+ // Security: Validate message size
160+ if ( message && message . length > maxMessageSize ) {
161+ debugError ( 'Message exceeds max size:' , message . length , '>' , maxMessageSize ) ;
162+ return ;
163+ }
164+
22165 if ( socketId ) {
23166 let socket = sockets [ socketId ] ;
24167 if ( action === 'close' ) {
@@ -29,6 +172,27 @@ export function createWebHandler({ myHostName, localHost, port, mqConn }) {
29172 }
30173 return ;
31174 } else if ( ! socket ) {
175+ // Security: Check concurrent socket limit
176+ const currentSocketCount = Object . keys ( sockets ) . length ;
177+ if ( currentSocketCount >= maxConcurrentSockets ) {
178+ debugError ( 'Max concurrent sockets reached:' , currentSocketCount ) ;
179+ return ;
180+ }
181+
182+ // Security: Validate initial HTTP request format
183+ if ( validateRequests && message ) {
184+ const validation = validateHttpRequest ( message ) ;
185+ if ( ! validation . valid ) {
186+ debugError ( 'Invalid request rejected:' , validation . reason ) ;
187+ return ;
188+ }
189+ // For NEW sockets, we require a valid HTTP request (not continuation data)
190+ if ( ! validation . isInitialRequest ) {
191+ debugError ( 'Non-HTTP initial request rejected' ) ;
192+ return ;
193+ }
194+ }
195+
32196 socket = new net . Socket ( ) ;
33197 socket . socketId = socketId ;
34198 sockets [ socketId ] = socket ;
@@ -60,12 +224,24 @@ export function createWebHandler({ myHostName, localHost, port, mqConn }) {
60224 return ;
61225 }
62226
227+ // Security: For existing sockets, still validate message size (already done above)
228+ // and check it's a Buffer
229+ if ( ! Buffer . isBuffer ( message ) ) {
230+ debugError ( 'Invalid message type for existing socket' ) ;
231+ return ;
232+ }
233+
63234 debug ( '←' , socketId , message . length ) ;
64235 socket . write ( message ) ;
65236 }
66237 }
67238
68239 function end ( ) {
240+ // Clear rate limiter cleanup interval
241+ if ( cleanupInterval ) {
242+ clearInterval ( cleanupInterval ) ;
243+ }
244+
69245 const sockKeys = Object . keys ( sockets ) ;
70246 sockKeys . forEach ( ( sk ) => {
71247 try {
@@ -77,9 +253,24 @@ export function createWebHandler({ myHostName, localHost, port, mqConn }) {
77253 } ) ;
78254 }
79255
256+ /**
257+ * Get current security stats for monitoring
258+ */
259+ function getStats ( ) {
260+ return {
261+ activeSockets : Object . keys ( sockets ) . length ,
262+ maxConcurrentSockets,
263+ maxMessageSize,
264+ rateLimitingEnabled : enableRateLimiting ,
265+ } ;
266+ }
267+
80268 return {
81269 handleWebRequest,
82270 sockets,
83271 end,
272+ getStats,
273+ // Exported for testing
274+ validateHttpRequest,
84275 } ;
85276}
0 commit comments