@@ -101,7 +101,8 @@ describe('serialize deserialize', { timeout: 10000 }, function () {
101101 ) ;
102102 const deserialized = await deserialize ( serialized ) ;
103103 assert . equal ( typeof deserialized , 'function' ) ;
104- assert . equal ( deserialized . name , '' ) ;
104+ // Empty function names are sanitized to 'anonymous' for security
105+ assert . equal ( deserialized . name , 'anonymous' ) ;
105106 } ) ;
106107
107108 it ( 'handles Text nodes' , async ( ) => {
@@ -200,12 +201,6 @@ describe('serialize deserialize', { timeout: 10000 }, function () {
200201 assert . deepEqual ( deserialized , [ 1 , 2 , 3 ] ) ;
201202 } ) ;
202203
203- it ( 'handles objects' , async ( ) => {
204- const serialized = await page . evaluate ( ( ) => window [ '_serialize' ] ( { a : 1 , b : 2 } ) ) ;
205- const deserialized = await deserialize ( serialized ) ;
206- assert . deepEqual ( deserialized , { a : 1 , b : 2 } ) ;
207- } ) ;
208-
209204 it ( 'handles objects with methods' , async ( ) => {
210205 const serialized = await page . evaluate ( ( ) =>
211206 window [ '_serialize' ] ( {
@@ -229,7 +224,8 @@ describe('serialize deserialize', { timeout: 10000 }, function () {
229224 assert . equal ( typeof deserialized . baz , 'function' ) ;
230225 assert . equal ( deserialized . baz . name , 'baz' ) ;
231226 assert . equal ( typeof deserialized [ 'my-element' ] , 'function' ) ;
232- assert . equal ( deserialized [ 'my-element' ] . name , 'my-element' ) ;
227+ // Names with hyphens are not valid JS identifiers, sanitized to 'anonymous' for security
228+ assert . equal ( deserialized [ 'my-element' ] . name , 'anonymous' ) ;
233229 } ) ;
234230
235231 it ( 'handles deep objects' , async ( ) => {
@@ -537,3 +533,243 @@ describe('serialize deserialize', { timeout: 10000 }, function () {
537533 assert . deepEqual ( deserialized , null ) ;
538534 } ) ;
539535} ) ;
536+
537+ describe ( 'deserialize security' , function ( ) {
538+ describe ( 'constructor name injection prevention' , function ( ) {
539+ it ( 'sanitizes constructor names with code injection payloads' , async ( ) => {
540+ // This payload would execute arbitrary code if interpolated directly into new Function(...)
541+ const maliciousPayload = JSON . stringify ( {
542+ __WTR_CONSTRUCTOR_NAME__ : 'x(){},(globalThis.PWNED=true),function y' ,
543+ data : 'test' ,
544+ } ) ;
545+
546+ const deserialized = await deserialize ( maliciousPayload ) ;
547+
548+ // The malicious code should NOT have executed
549+ assert . equal ( ( globalThis as any ) . PWNED , undefined ) ;
550+ // The object should still be deserialized with a safe fallback name
551+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
552+ assert . equal ( deserialized . data , 'test' ) ;
553+ } ) ;
554+
555+ it ( 'sanitizes constructor names with require() injection' , async ( ) => {
556+ const maliciousPayload = JSON . stringify ( {
557+ __WTR_CONSTRUCTOR_NAME__ :
558+ 'x(){},require("child_process").execSync("echo PWNED"),function y' ,
559+ value : 123 ,
560+ } ) ;
561+
562+ const deserialized = await deserialize ( maliciousPayload ) ;
563+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
564+ assert . equal ( deserialized . value , 123 ) ;
565+ } ) ;
566+
567+ it ( 'sanitizes constructor names with parentheses' , async ( ) => {
568+ const maliciousPayload = JSON . stringify ( {
569+ __WTR_CONSTRUCTOR_NAME__ : 'Foo()' ,
570+ } ) ;
571+
572+ const deserialized = await deserialize ( maliciousPayload ) ;
573+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
574+ } ) ;
575+
576+ it ( 'sanitizes constructor names with spaces' , async ( ) => {
577+ const maliciousPayload = JSON . stringify ( {
578+ __WTR_CONSTRUCTOR_NAME__ : 'Foo Bar' ,
579+ } ) ;
580+
581+ const deserialized = await deserialize ( maliciousPayload ) ;
582+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
583+ } ) ;
584+
585+ it ( 'sanitizes constructor names with semicolons' , async ( ) => {
586+ const maliciousPayload = JSON . stringify ( {
587+ __WTR_CONSTRUCTOR_NAME__ : 'Foo;console.log("pwned")' ,
588+ } ) ;
589+
590+ const deserialized = await deserialize ( maliciousPayload ) ;
591+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
592+ } ) ;
593+
594+ it ( 'sanitizes constructor names with curly braces' , async ( ) => {
595+ const maliciousPayload = JSON . stringify ( {
596+ __WTR_CONSTRUCTOR_NAME__ : 'Foo{}' ,
597+ } ) ;
598+
599+ const deserialized = await deserialize ( maliciousPayload ) ;
600+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
601+ } ) ;
602+
603+ it ( 'sanitizes constructor names with commas' , async ( ) => {
604+ const maliciousPayload = JSON . stringify ( {
605+ __WTR_CONSTRUCTOR_NAME__ : 'x,y' ,
606+ } ) ;
607+
608+ const deserialized = await deserialize ( maliciousPayload ) ;
609+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
610+ } ) ;
611+
612+ it ( 'sanitizes empty constructor names' , async ( ) => {
613+ const maliciousPayload = JSON . stringify ( {
614+ __WTR_CONSTRUCTOR_NAME__ : '' ,
615+ } ) ;
616+
617+ const deserialized = await deserialize ( maliciousPayload ) ;
618+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
619+ } ) ;
620+
621+ it ( 'sanitizes constructor names starting with digits' , async ( ) => {
622+ const maliciousPayload = JSON . stringify ( {
623+ __WTR_CONSTRUCTOR_NAME__ : '123Foo' ,
624+ } ) ;
625+
626+ const deserialized = await deserialize ( maliciousPayload ) ;
627+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
628+ } ) ;
629+
630+ it ( 'allows valid constructor names' , async ( ) => {
631+ const validNames = [ 'Foo' , 'FooBar' , '_Foo' , '$Foo' , 'Foo123' , '_$foo_bar_123' ] ;
632+
633+ for ( const name of validNames ) {
634+ const payload = JSON . stringify ( {
635+ __WTR_CONSTRUCTOR_NAME__ : name ,
636+ } ) ;
637+
638+ const deserialized = await deserialize ( payload ) ;
639+ assert . equal ( deserialized . constructor . name , name , `Expected ${ name } to be allowed` ) ;
640+ }
641+ } ) ;
642+ } ) ;
643+
644+ describe ( 'function name injection prevention' , function ( ) {
645+ it ( 'sanitizes function names with code injection payloads' , async ( ) => {
646+ const maliciousPayload = JSON . stringify ( {
647+ __WTR_TYPE__ : 'Function' ,
648+ name : 'x(){},(globalThis.PWNED_FN=true),function y' ,
649+ } ) ;
650+
651+ const deserialized = await deserialize ( maliciousPayload ) ;
652+
653+ // The malicious code should NOT have executed
654+ assert . equal ( ( globalThis as any ) . PWNED_FN , undefined ) ;
655+ // The function should still be deserialized with a safe fallback name
656+ assert . equal ( typeof deserialized , 'function' ) ;
657+ assert . equal ( deserialized . name , 'anonymous' ) ;
658+ } ) ;
659+
660+ it ( 'sanitizes function names with require() injection' , async ( ) => {
661+ const maliciousPayload = JSON . stringify ( {
662+ __WTR_TYPE__ : 'Function' ,
663+ name : 'x(){},require("child_process"),function y' ,
664+ } ) ;
665+
666+ const deserialized = await deserialize ( maliciousPayload ) ;
667+ assert . equal ( typeof deserialized , 'function' ) ;
668+ assert . equal ( deserialized . name , 'anonymous' ) ;
669+ } ) ;
670+
671+ it ( 'sanitizes function names with parentheses' , async ( ) => {
672+ const maliciousPayload = JSON . stringify ( {
673+ __WTR_TYPE__ : 'Function' ,
674+ name : 'foo()' ,
675+ } ) ;
676+
677+ const deserialized = await deserialize ( maliciousPayload ) ;
678+ assert . equal ( typeof deserialized , 'function' ) ;
679+ assert . equal ( deserialized . name , 'anonymous' ) ;
680+ } ) ;
681+
682+ it ( 'sanitizes function names with spaces' , async ( ) => {
683+ const maliciousPayload = JSON . stringify ( {
684+ __WTR_TYPE__ : 'Function' ,
685+ name : 'foo bar' ,
686+ } ) ;
687+
688+ const deserialized = await deserialize ( maliciousPayload ) ;
689+ assert . equal ( typeof deserialized , 'function' ) ;
690+ assert . equal ( deserialized . name , 'anonymous' ) ;
691+ } ) ;
692+
693+ it ( 'sanitizes empty function names' , async ( ) => {
694+ const maliciousPayload = JSON . stringify ( {
695+ __WTR_TYPE__ : 'Function' ,
696+ name : '' ,
697+ } ) ;
698+
699+ const deserialized = await deserialize ( maliciousPayload ) ;
700+ assert . equal ( typeof deserialized , 'function' ) ;
701+ assert . equal ( deserialized . name , 'anonymous' ) ;
702+ } ) ;
703+
704+ it ( 'handles bound function prefix with malicious payload' , async ( ) => {
705+ const maliciousPayload = JSON . stringify ( {
706+ __WTR_TYPE__ : 'Function' ,
707+ name : 'bound x(){},(globalThis.PWNED_BOUND=true),function y' ,
708+ } ) ;
709+
710+ const deserialized = await deserialize ( maliciousPayload ) ;
711+
712+ assert . equal ( ( globalThis as any ) . PWNED_BOUND , undefined ) ;
713+ assert . equal ( typeof deserialized , 'function' ) ;
714+ assert . equal ( deserialized . name , 'anonymous' ) ;
715+ } ) ;
716+
717+ it ( 'allows valid function names' , async ( ) => {
718+ const validNames = [ 'foo' , 'fooBar' , '_foo' , '$foo' , 'foo123' , '_$foo_bar_123' ] ;
719+
720+ for ( const name of validNames ) {
721+ const payload = JSON . stringify ( {
722+ __WTR_TYPE__ : 'Function' ,
723+ name : name ,
724+ } ) ;
725+
726+ const deserialized = await deserialize ( payload ) ;
727+ assert . equal ( typeof deserialized , 'function' ) ;
728+ assert . equal ( deserialized . name , name , `Expected ${ name } to be allowed` ) ;
729+ }
730+ } ) ;
731+
732+ it ( 'allows valid bound function names' , async ( ) => {
733+ const payload = JSON . stringify ( {
734+ __WTR_TYPE__ : 'Function' ,
735+ name : 'bound myFunction' ,
736+ } ) ;
737+
738+ const deserialized = await deserialize ( payload ) ;
739+ assert . equal ( typeof deserialized , 'function' ) ;
740+ assert . equal ( deserialized . name , 'myFunction' ) ;
741+ } ) ;
742+ } ) ;
743+
744+ describe ( 'nested injection prevention' , function ( ) {
745+ it ( 'sanitizes malicious names in nested objects' , async ( ) => {
746+ const maliciousPayload = JSON . stringify ( {
747+ nested : {
748+ __WTR_CONSTRUCTOR_NAME__ : 'x(){},(globalThis.NESTED_PWNED=true),function y' ,
749+ data : 'nested data' ,
750+ } ,
751+ } ) ;
752+
753+ const deserialized = await deserialize ( maliciousPayload ) ;
754+
755+ assert . equal ( ( globalThis as any ) . NESTED_PWNED , undefined ) ;
756+ assert . equal ( deserialized . nested . constructor . name , '__unknown__' ) ;
757+ assert . equal ( deserialized . nested . data , 'nested data' ) ;
758+ } ) ;
759+
760+ it ( 'sanitizes malicious function names in arrays' , async ( ) => {
761+ const maliciousPayload = JSON . stringify ( [
762+ {
763+ __WTR_TYPE__ : 'Function' ,
764+ name : 'x(){},(globalThis.ARRAY_PWNED=true),function y' ,
765+ } ,
766+ ] ) ;
767+
768+ const deserialized = await deserialize ( maliciousPayload ) ;
769+
770+ assert . equal ( ( globalThis as any ) . ARRAY_PWNED , undefined ) ;
771+ assert . equal ( typeof deserialized [ 0 ] , 'function' ) ;
772+ assert . equal ( deserialized [ 0 ] . name , 'anonymous' ) ;
773+ } ) ;
774+ } ) ;
775+ } ) ;
0 commit comments