1414 * limitations under the License.
1515 */
1616
17+ import type { Context } from "vm" ;
18+
1719import {
20+ getJazzerJsGlobal ,
1821 guideTowardsContainment ,
1922 reportAndThrowFinding ,
2023} from "@jazzer.js/core" ;
21- import { callSiteId , registerBeforeHook } from "@jazzer.js/hooking" ;
24+ import { registerBeforeHook } from "@jazzer.js/hooking" ;
25+
26+ const CANARY_NAME = "jaz_zer" ;
27+
28+ function createCanaryDescriptor ( canaryName : string ) : PropertyDescriptor {
29+ return {
30+ get ( ) {
31+ reportAndThrowFinding (
32+ "Remote Code Execution\n" +
33+ ` attacker-controlled code accessed globalThis.${ canaryName } ` ,
34+ ) ;
35+ } ,
36+ enumerable : false ,
37+ configurable : false ,
38+ } ;
39+ }
40+
41+ function installCanaryIfMissing (
42+ target : object ,
43+ canaryName : string ,
44+ descriptor : PropertyDescriptor ,
45+ ) : void {
46+ if ( Object . getOwnPropertyDescriptor ( target , canaryName ) ) {
47+ return ;
48+ }
49+ Object . defineProperty ( target , canaryName , descriptor ) ;
50+ }
51+
52+ // The canary should be present in both globals used by Jazzer.js:
53+ // - globalThis in CLI mode
54+ // - vmContext in Jest mode
55+ const canaryDescriptor = createCanaryDescriptor ( CANARY_NAME ) ;
56+ installCanaryIfMissing ( globalThis , CANARY_NAME , canaryDescriptor ) ;
57+
58+ const vmContext = getJazzerJsGlobal < Context > ( "vmContext" ) ;
59+ if ( vmContext ) {
60+ installCanaryIfMissing ( vmContext , CANARY_NAME , canaryDescriptor ) ;
61+ }
2262
23- const targetString = "jaz_zer" ;
63+ // Guidance: before-hooks steer the fuzzer toward getting the canary name into
64+ // eval/Function bodies. A finding is reported when compiled code reads it.
2465
2566registerBeforeHook (
2667 "eval" ,
2768 "" ,
2869 false ,
29- function beforeEvalHook ( _thisPtr : unknown , params : string [ ] , hookId : number ) {
70+ function beforeEvalHook (
71+ _thisPtr : unknown ,
72+ params : unknown [ ] ,
73+ hookId : number ,
74+ ) {
3075 const code = params [ 0 ] ;
31- // This check will prevent runtime TypeErrors should the user decide to call Function with
32- // non-string arguments.
33- // noinspection SuspiciousTypeOfGuard
34- if ( typeof code === "string" && code . includes ( targetString ) ) {
35- reportAndThrowFinding (
36- "Remote Code Execution\n" + ` using eval:\n '${ code } '` ,
37- ) ;
76+ // eval with non-string arguments is a no-op (returns the argument as-is),
77+ // so guidance is only meaningful for actual strings.
78+ if ( typeof code === "string" ) {
79+ guideTowardsContainment ( code , CANARY_NAME , hookId ) ;
3880 }
39-
40- // Since we do not hook eval using the hooking framework, we have to recompute the
41- // call site ID on every call to eval. This shouldn't be an issue, because eval is
42- // considered evil and should not be called too often, or even better -- not at all!
43- guideTowardsContainment ( code , targetString , hookId ) ;
4481 } ,
4582) ;
4683
@@ -50,22 +87,27 @@ registerBeforeHook(
5087 false ,
5188 function beforeFunctionHook (
5289 _thisPtr : unknown ,
53- params : string [ ] ,
90+ params : unknown [ ] ,
5491 hookId : number ,
5592 ) {
56- if ( params . length > 0 ) {
57- const functionBody = params [ params . length - 1 ] ;
93+ if ( params . length === 0 ) return ;
5894
59- // noinspection SuspiciousTypeOfGuard
60- if ( typeof functionBody === "string" ) {
61- if ( functionBody . includes ( targetString ) ) {
62- reportAndThrowFinding (
63- "Remote Code Execution\n" +
64- ` using Function:\n '${ functionBody } '` ,
65- ) ;
66- }
67- guideTowardsContainment ( functionBody , targetString , hookId ) ;
68- }
95+ // The Function constructor coerces every argument to string via ToString().
96+ // Template engines (e.g. Handlebars) pass non-string objects like SourceNode
97+ // whose toString() yields executable code. Coerce here to match V8's
98+ // behavior so guidance works for those cases too.
99+ const functionBody = params [ params . length - 1 ] ;
100+ if ( functionBody == null ) return ;
101+
102+ let functionBodySource : string ;
103+ try {
104+ functionBodySource = String ( functionBody ) ;
105+ } catch {
106+ // toString() would also throw inside the Function constructor, so
107+ // no code will be compiled, no RCE risk, no guidance needed.
108+ return ;
69109 }
110+
111+ guideTowardsContainment ( functionBodySource , CANARY_NAME , hookId ) ;
70112 } ,
71113) ;
0 commit comments