@@ -180,4 +180,226 @@ describe('webhooks router', () => {
180180
181181    await  agent . delete ( '/webhooks/invalid-id' ) . expect ( 400 ) ; 
182182  } ) ; 
183+ 
184+   describe ( 'Header validation' ,  ( )  =>  { 
185+     it ( 'POST / - accepts valid header names' ,  async  ( )  =>  { 
186+       const  {  agent }  =  await  getLoggedInAgent ( server ) ; 
187+ 
188+       const  validHeaders  =  { 
189+         'Content-Type' : 'application/json' , 
190+         Authorization : 'Bearer token' , 
191+         'X-Custom-Header' : 'value' , 
192+         'User-Agent' : 'test' , 
193+         'x-api-key' : 'secret' , 
194+         'custom!header#test' : 'value' , 
195+       } ; 
196+ 
197+       const  response  =  await  agent 
198+         . post ( '/webhooks' ) 
199+         . send ( { 
200+           ...MOCK_WEBHOOK , 
201+           url : 'https://example.com/valid-headers' , 
202+           headers : validHeaders , 
203+         } ) 
204+         . expect ( 200 ) ; 
205+ 
206+       expect ( response . body . data . headers ) . toMatchObject ( validHeaders ) ; 
207+     } ) ; 
208+ 
209+     it ( 'POST / - rejects header names starting with numbers' ,  async  ( )  =>  { 
210+       const  {  agent }  =  await  getLoggedInAgent ( server ) ; 
211+ 
212+       const  response  =  await  agent 
213+         . post ( '/webhooks' ) 
214+         . send ( { 
215+           ...MOCK_WEBHOOK , 
216+           url : 'https://example.com/invalid-header-name' , 
217+           headers : { 
218+             '123Invalid' : 'value' , 
219+           } , 
220+         } ) 
221+         . expect ( 400 ) ; 
222+ 
223+       expect ( Array . isArray ( response . body ) ) . toBe ( true ) ; 
224+       expect ( response . body [ 0 ] . type ) . toBe ( 'Body' ) ; 
225+       expect ( response . body [ 0 ] . errors ) . toBeDefined ( ) ; 
226+     } ) ; 
227+ 
228+     it ( 'POST / - rejects empty header names' ,  async  ( )  =>  { 
229+       const  {  agent }  =  await  getLoggedInAgent ( server ) ; 
230+ 
231+       const  response  =  await  agent 
232+         . post ( '/webhooks' ) 
233+         . send ( { 
234+           ...MOCK_WEBHOOK , 
235+           url : 'https://example.com/empty-header-name' , 
236+           headers : { 
237+             '' : 'value' , 
238+           } , 
239+         } ) 
240+         . expect ( 400 ) ; 
241+ 
242+       expect ( Array . isArray ( response . body ) ) . toBe ( true ) ; 
243+       expect ( response . body [ 0 ] . type ) . toBe ( 'Body' ) ; 
244+       expect ( response . body [ 0 ] . errors ) . toBeDefined ( ) ; 
245+     } ) ; 
246+ 
247+     it ( 'POST / - rejects header names with invalid characters' ,  async  ( )  =>  { 
248+       const  {  agent }  =  await  getLoggedInAgent ( server ) ; 
249+ 
250+       const  invalidHeaderNames  =  [ 
251+         {  'Header Name' : 'value'  } ,  // space 
252+         {  'Header\nName' : 'value'  } ,  // newline 
253+         {  'Header\rName' : 'value'  } ,  // carriage return 
254+         {  'Header\tName' : 'value'  } ,  // tab 
255+         {  'Header@Name' : 'value'  } ,  // @ not allowed 
256+         {  'Header[Name]' : 'value'  } ,  // brackets not allowed 
257+       ] ; 
258+ 
259+       for  ( const  headers  of  invalidHeaderNames )  { 
260+         const  response  =  await  agent 
261+           . post ( '/webhooks' ) 
262+           . send ( { 
263+             ...MOCK_WEBHOOK , 
264+             url : `https://example.com/invalid-header-${ Math . random ( ) }  , 
265+             headers, 
266+           } ) 
267+           . expect ( 400 ) ; 
268+ 
269+         expect ( Array . isArray ( response . body ) ) . toBe ( true ) ; 
270+         expect ( response . body [ 0 ] . type ) . toBe ( 'Body' ) ; 
271+         expect ( response . body [ 0 ] . errors ) . toBeDefined ( ) ; 
272+       } 
273+     } ) ; 
274+ 
275+     it ( 'POST / - accepts valid header values' ,  async  ( )  =>  { 
276+       const  {  agent }  =  await  getLoggedInAgent ( server ) ; 
277+ 
278+       const  validHeaders  =  { 
279+         Authorization : 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' , 
280+         'Content-Type' : 'application/json; charset=utf-8' , 
281+         'X-Api-Key' : 'abc123-def456-ghi789' , 
282+         'User-Agent' : 'Mozilla/5.0 (compatible; TestBot/1.0)' , 
283+         'Custom-Header' : 'value with spaces and special chars: !@#$%^&*()' , 
284+       } ; 
285+ 
286+       const  response  =  await  agent 
287+         . post ( '/webhooks' ) 
288+         . send ( { 
289+           ...MOCK_WEBHOOK , 
290+           url : 'https://example.com/valid-header-values' , 
291+           headers : validHeaders , 
292+         } ) 
293+         . expect ( 200 ) ; 
294+ 
295+       expect ( response . body . data . headers ) . toMatchObject ( validHeaders ) ; 
296+     } ) ; 
297+ 
298+     it ( 'POST / - rejects header values with CRLF injection' ,  async  ( )  =>  { 
299+       const  {  agent }  =  await  getLoggedInAgent ( server ) ; 
300+ 
301+       const  response  =  await  agent 
302+         . post ( '/webhooks' ) 
303+         . send ( { 
304+           ...MOCK_WEBHOOK , 
305+           url : 'https://example.com/crlf-injection' , 
306+           headers : { 
307+             'X-Custom-Header' : 'value\r\nX-Injected-Header: malicious' , 
308+           } , 
309+         } ) 
310+         . expect ( 400 ) ; 
311+ 
312+       expect ( Array . isArray ( response . body ) ) . toBe ( true ) ; 
313+       expect ( response . body [ 0 ] . type ) . toBe ( 'Body' ) ; 
314+       expect ( response . body [ 0 ] . errors ) . toBeDefined ( ) ; 
315+     } ) ; 
316+ 
317+     it ( 'POST / - rejects header values with tab characters' ,  async  ( )  =>  { 
318+       const  {  agent }  =  await  getLoggedInAgent ( server ) ; 
319+ 
320+       const  response  =  await  agent 
321+         . post ( '/webhooks' ) 
322+         . send ( { 
323+           ...MOCK_WEBHOOK , 
324+           url : 'https://example.com/tab-injection' , 
325+           headers : { 
326+             'X-Custom-Header' : 'value\twith\ttabs' , 
327+           } , 
328+         } ) 
329+         . expect ( 400 ) ; 
330+ 
331+       expect ( Array . isArray ( response . body ) ) . toBe ( true ) ; 
332+       expect ( response . body [ 0 ] . type ) . toBe ( 'Body' ) ; 
333+       expect ( response . body [ 0 ] . errors ) . toBeDefined ( ) ; 
334+     } ) ; 
335+ 
336+     it ( 'POST / - rejects header values with control characters' ,  async  ( )  =>  { 
337+       const  {  agent }  =  await  getLoggedInAgent ( server ) ; 
338+ 
339+       // Test various control characters 
340+       const  controlCharTests  =  [ 
341+         '\x00' ,  // null 
342+         '\x01' ,  // start of heading 
343+         '\x0B' ,  // vertical tab 
344+         '\x0C' ,  // form feed 
345+         '\x1F' ,  // unit separator 
346+         '\x7F' ,  // delete 
347+       ] ; 
348+ 
349+       for  ( const  controlChar  of  controlCharTests )  { 
350+         const  response  =  await  agent 
351+           . post ( '/webhooks' ) 
352+           . send ( { 
353+             ...MOCK_WEBHOOK , 
354+             url : `https://example.com/control-char-${ Math . random ( ) }  , 
355+             headers : { 
356+               'X-Custom-Header' : `value${ controlChar }  , 
357+             } , 
358+           } ) 
359+           . expect ( 400 ) ; 
360+ 
361+         expect ( Array . isArray ( response . body ) ) . toBe ( true ) ; 
362+         expect ( response . body [ 0 ] . type ) . toBe ( 'Body' ) ; 
363+         expect ( response . body [ 0 ] . errors ) . toBeDefined ( ) ; 
364+       } 
365+     } ) ; 
366+ 
367+     it ( 'POST / - rejects header values with newline characters' ,  async  ( )  =>  { 
368+       const  {  agent }  =  await  getLoggedInAgent ( server ) ; 
369+ 
370+       const  response  =  await  agent 
371+         . post ( '/webhooks' ) 
372+         . send ( { 
373+           ...MOCK_WEBHOOK , 
374+           url : 'https://example.com/newline-injection' , 
375+           headers : { 
376+             'X-Custom-Header' : 'value\nwith\nnewlines' , 
377+           } , 
378+         } ) 
379+         . expect ( 400 ) ; 
380+ 
381+       expect ( Array . isArray ( response . body ) ) . toBe ( true ) ; 
382+       expect ( response . body [ 0 ] . type ) . toBe ( 'Body' ) ; 
383+       expect ( response . body [ 0 ] . errors ) . toBeDefined ( ) ; 
384+     } ) ; 
385+ 
386+     it ( 'POST / - rejects header values with carriage return characters' ,  async  ( )  =>  { 
387+       const  {  agent }  =  await  getLoggedInAgent ( server ) ; 
388+ 
389+       const  response  =  await  agent 
390+         . post ( '/webhooks' ) 
391+         . send ( { 
392+           ...MOCK_WEBHOOK , 
393+           url : 'https://example.com/carriage-return-injection' , 
394+           headers : { 
395+             'X-Custom-Header' : 'value\rwith\rcarriage\rreturns' , 
396+           } , 
397+         } ) 
398+         . expect ( 400 ) ; 
399+ 
400+       expect ( Array . isArray ( response . body ) ) . toBe ( true ) ; 
401+       expect ( response . body [ 0 ] . type ) . toBe ( 'Body' ) ; 
402+       expect ( response . body [ 0 ] . errors ) . toBeDefined ( ) ; 
403+     } ) ; 
404+   } ) ; 
183405} ) ; 
0 commit comments