@@ -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,244 @@ 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__ :
543+ 'x(){},(globalThis.PWNED=true),function y' ,
544+ data : 'test' ,
545+ } ) ;
546+
547+ const deserialized = await deserialize ( maliciousPayload ) ;
548+
549+ // The malicious code should NOT have executed
550+ assert . equal ( ( globalThis as any ) . PWNED , undefined ) ;
551+ // The object should still be deserialized with a safe fallback name
552+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
553+ assert . equal ( deserialized . data , 'test' ) ;
554+ } ) ;
555+
556+ it ( 'sanitizes constructor names with require() injection' , async ( ) => {
557+ const maliciousPayload = JSON . stringify ( {
558+ __WTR_CONSTRUCTOR_NAME__ :
559+ 'x(){},require("child_process").execSync("echo PWNED"),function y' ,
560+ value : 123 ,
561+ } ) ;
562+
563+ const deserialized = await deserialize ( maliciousPayload ) ;
564+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
565+ assert . equal ( deserialized . value , 123 ) ;
566+ } ) ;
567+
568+ it ( 'sanitizes constructor names with parentheses' , async ( ) => {
569+ const maliciousPayload = JSON . stringify ( {
570+ __WTR_CONSTRUCTOR_NAME__ : 'Foo()' ,
571+ } ) ;
572+
573+ const deserialized = await deserialize ( maliciousPayload ) ;
574+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
575+ } ) ;
576+
577+ it ( 'sanitizes constructor names with spaces' , async ( ) => {
578+ const maliciousPayload = JSON . stringify ( {
579+ __WTR_CONSTRUCTOR_NAME__ : 'Foo Bar' ,
580+ } ) ;
581+
582+ const deserialized = await deserialize ( maliciousPayload ) ;
583+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
584+ } ) ;
585+
586+ it ( 'sanitizes constructor names with semicolons' , async ( ) => {
587+ const maliciousPayload = JSON . stringify ( {
588+ __WTR_CONSTRUCTOR_NAME__ : 'Foo;console.log("pwned")' ,
589+ } ) ;
590+
591+ const deserialized = await deserialize ( maliciousPayload ) ;
592+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
593+ } ) ;
594+
595+ it ( 'sanitizes constructor names with curly braces' , async ( ) => {
596+ const maliciousPayload = JSON . stringify ( {
597+ __WTR_CONSTRUCTOR_NAME__ : 'Foo{}' ,
598+ } ) ;
599+
600+ const deserialized = await deserialize ( maliciousPayload ) ;
601+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
602+ } ) ;
603+
604+ it ( 'sanitizes constructor names with commas' , async ( ) => {
605+ const maliciousPayload = JSON . stringify ( {
606+ __WTR_CONSTRUCTOR_NAME__ : 'x,y' ,
607+ } ) ;
608+
609+ const deserialized = await deserialize ( maliciousPayload ) ;
610+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
611+ } ) ;
612+
613+ it ( 'sanitizes empty constructor names' , async ( ) => {
614+ const maliciousPayload = JSON . stringify ( {
615+ __WTR_CONSTRUCTOR_NAME__ : '' ,
616+ } ) ;
617+
618+ const deserialized = await deserialize ( maliciousPayload ) ;
619+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
620+ } ) ;
621+
622+ it ( 'sanitizes constructor names starting with digits' , async ( ) => {
623+ const maliciousPayload = JSON . stringify ( {
624+ __WTR_CONSTRUCTOR_NAME__ : '123Foo' ,
625+ } ) ;
626+
627+ const deserialized = await deserialize ( maliciousPayload ) ;
628+ assert . equal ( deserialized . constructor . name , '__unknown__' ) ;
629+ } ) ;
630+
631+ it ( 'allows valid constructor names' , async ( ) => {
632+ const validNames = [ 'Foo' , 'FooBar' , '_Foo' , '$Foo' , 'Foo123' , '_$foo_bar_123' ] ;
633+
634+ for ( const name of validNames ) {
635+ const payload = JSON . stringify ( {
636+ __WTR_CONSTRUCTOR_NAME__ : name ,
637+ } ) ;
638+
639+ const deserialized = await deserialize ( payload ) ;
640+ assert . equal ( deserialized . constructor . name , name , `Expected ${ name } to be allowed` ) ;
641+ }
642+ } ) ;
643+ } ) ;
644+
645+ describe ( 'function name injection prevention' , function ( ) {
646+ it ( 'sanitizes function names with code injection payloads' , async ( ) => {
647+ const maliciousPayload = JSON . stringify ( {
648+ __WTR_TYPE__ : 'Function' ,
649+ name : 'x(){},(globalThis.PWNED_FN=true),function y' ,
650+ } ) ;
651+
652+ const deserialized = await deserialize ( maliciousPayload ) ;
653+
654+ // The malicious code should NOT have executed
655+ assert . equal ( ( globalThis as any ) . PWNED_FN , undefined ) ;
656+ // The function should still be deserialized with a safe fallback name
657+ assert . equal ( typeof deserialized , 'function' ) ;
658+ assert . equal ( deserialized . name , 'anonymous' ) ;
659+ } ) ;
660+
661+ it ( 'sanitizes function names with require() injection' , async ( ) => {
662+ const maliciousPayload = JSON . stringify ( {
663+ __WTR_TYPE__ : 'Function' ,
664+ name : 'x(){},require("child_process"),function y' ,
665+ } ) ;
666+
667+ const deserialized = await deserialize ( maliciousPayload ) ;
668+ assert . equal ( typeof deserialized , 'function' ) ;
669+ assert . equal ( deserialized . name , 'anonymous' ) ;
670+ } ) ;
671+
672+ it ( 'sanitizes function names with parentheses' , async ( ) => {
673+ const maliciousPayload = JSON . stringify ( {
674+ __WTR_TYPE__ : 'Function' ,
675+ name : 'foo()' ,
676+ } ) ;
677+
678+ const deserialized = await deserialize ( maliciousPayload ) ;
679+ assert . equal ( typeof deserialized , 'function' ) ;
680+ assert . equal ( deserialized . name , 'anonymous' ) ;
681+ } ) ;
682+
683+ it ( 'sanitizes function names with spaces' , async ( ) => {
684+ const maliciousPayload = JSON . stringify ( {
685+ __WTR_TYPE__ : 'Function' ,
686+ name : 'foo bar' ,
687+ } ) ;
688+
689+ const deserialized = await deserialize ( maliciousPayload ) ;
690+ assert . equal ( typeof deserialized , 'function' ) ;
691+ assert . equal ( deserialized . name , 'anonymous' ) ;
692+ } ) ;
693+
694+ it ( 'sanitizes empty function names' , async ( ) => {
695+ const maliciousPayload = JSON . stringify ( {
696+ __WTR_TYPE__ : 'Function' ,
697+ name : '' ,
698+ } ) ;
699+
700+ const deserialized = await deserialize ( maliciousPayload ) ;
701+ assert . equal ( typeof deserialized , 'function' ) ;
702+ assert . equal ( deserialized . name , 'anonymous' ) ;
703+ } ) ;
704+
705+ it ( 'handles bound function prefix with malicious payload' , async ( ) => {
706+ const maliciousPayload = JSON . stringify ( {
707+ __WTR_TYPE__ : 'Function' ,
708+ name : 'bound x(){},(globalThis.PWNED_BOUND=true),function y' ,
709+ } ) ;
710+
711+ const deserialized = await deserialize ( maliciousPayload ) ;
712+
713+ assert . equal ( ( globalThis as any ) . PWNED_BOUND , undefined ) ;
714+ assert . equal ( typeof deserialized , 'function' ) ;
715+ assert . equal ( deserialized . name , 'anonymous' ) ;
716+ } ) ;
717+
718+ it ( 'allows valid function names' , async ( ) => {
719+ const validNames = [ 'foo' , 'fooBar' , '_foo' , '$foo' , 'foo123' , '_$foo_bar_123' ] ;
720+
721+ for ( const name of validNames ) {
722+ const payload = JSON . stringify ( {
723+ __WTR_TYPE__ : 'Function' ,
724+ name : name ,
725+ } ) ;
726+
727+ const deserialized = await deserialize ( payload ) ;
728+ assert . equal ( typeof deserialized , 'function' ) ;
729+ assert . equal ( deserialized . name , name , `Expected ${ name } to be allowed` ) ;
730+ }
731+ } ) ;
732+
733+ it ( 'allows valid bound function names' , async ( ) => {
734+ const payload = JSON . stringify ( {
735+ __WTR_TYPE__ : 'Function' ,
736+ name : 'bound myFunction' ,
737+ } ) ;
738+
739+ const deserialized = await deserialize ( payload ) ;
740+ assert . equal ( typeof deserialized , 'function' ) ;
741+ assert . equal ( deserialized . name , 'myFunction' ) ;
742+ } ) ;
743+ } ) ;
744+
745+ describe ( 'nested injection prevention' , function ( ) {
746+ it ( 'sanitizes malicious names in nested objects' , async ( ) => {
747+ const maliciousPayload = JSON . stringify ( {
748+ nested : {
749+ __WTR_CONSTRUCTOR_NAME__ : 'x(){},(globalThis.NESTED_PWNED=true),function y' ,
750+ data : 'nested data' ,
751+ } ,
752+ } ) ;
753+
754+ const deserialized = await deserialize ( maliciousPayload ) ;
755+
756+ assert . equal ( ( globalThis as any ) . NESTED_PWNED , undefined ) ;
757+ assert . equal ( deserialized . nested . constructor . name , '__unknown__' ) ;
758+ assert . equal ( deserialized . nested . data , 'nested data' ) ;
759+ } ) ;
760+
761+ it ( 'sanitizes malicious function names in arrays' , async ( ) => {
762+ const maliciousPayload = JSON . stringify ( [
763+ {
764+ __WTR_TYPE__ : 'Function' ,
765+ name : 'x(){},(globalThis.ARRAY_PWNED=true),function y' ,
766+ } ,
767+ ] ) ;
768+
769+ const deserialized = await deserialize ( maliciousPayload ) ;
770+
771+ assert . equal ( ( globalThis as any ) . ARRAY_PWNED , undefined ) ;
772+ assert . equal ( typeof deserialized [ 0 ] , 'function' ) ;
773+ assert . equal ( deserialized [ 0 ] . name , 'anonymous' ) ;
774+ } ) ;
775+ } ) ;
776+ } ) ;
0 commit comments