@@ -25,13 +25,93 @@ import {
2525} from "@jazzer.js/core" ;
2626import { registerBeforeHook } from "@jazzer.js/hooking" ;
2727
28+ import { bugDetectorConfigurations } from "../configuration" ;
2829import { ensureCanary } from "../shared/code-injection-canary" ;
30+ import {
31+ buildGenericSuppressionSnippet ,
32+ captureStack ,
33+ getUserFacingStackLines ,
34+ IgnoreList ,
35+ type IgnoreRule ,
36+ } from "../shared/finding-suppression" ;
37+
38+ export type { IgnoreRule } from "../shared/finding-suppression" ;
2939
3040type PendingAccess = {
3141 canaryName : string ;
42+ stack : string ;
3243 invoked : boolean ;
3344} ;
3445
46+ /**
47+ * Configuration for the Code Injection bug detector.
48+ * Controls the reporting and suppression of dynamic code evaluation findings.
49+ */
50+ export interface CodeInjectionConfig {
51+ /**
52+ * Disables Stage 1 (Access) reporting entirely.
53+ * The detector will no longer report when the canary is merely read.
54+ */
55+ disableAccessReporting ( ) : this;
56+ /**
57+ * Disables Stage 2 (Invocation) reporting entirely.
58+ * The detector will no longer report when the canary is actually executed.
59+ */
60+ disableInvocationReporting ( ) : this;
61+ /**
62+ * Suppresses Stage 1 (Access) findings that match the provided rule.
63+ * Use this to silence safe heuristic reads such as template lookups.
64+ */
65+ ignoreAccess ( rule : IgnoreRule ) : this;
66+ /**
67+ * Suppresses Stage 2 (Invocation) findings that match the provided rule.
68+ * Use this only for known-safe execution sinks in test environments.
69+ */
70+ ignoreInvocation ( rule : IgnoreRule ) : this;
71+ }
72+
73+ class CodeInjectionConfigImpl implements CodeInjectionConfig {
74+ private _reportAccess = true ;
75+ private _reportInvocation = true ;
76+ private readonly _ignoredAccessRules = new IgnoreList ( ) ;
77+ private readonly _ignoredInvocationRules = new IgnoreList ( ) ;
78+
79+ disableAccessReporting ( ) : this {
80+ this . _reportAccess = false ;
81+ return this ;
82+ }
83+
84+ disableInvocationReporting ( ) : this {
85+ this . _reportInvocation = false ;
86+ return this ;
87+ }
88+
89+ ignoreAccess ( rule : IgnoreRule ) : this {
90+ this . _ignoredAccessRules . add ( rule ) ;
91+ return this ;
92+ }
93+
94+ ignoreInvocation ( rule : IgnoreRule ) : this {
95+ this . _ignoredInvocationRules . add ( rule ) ;
96+ return this ;
97+ }
98+
99+ shouldReportAccess ( stack : string ) : boolean {
100+ return this . _reportAccess && ! this . _ignoredAccessRules . matches ( stack ) ;
101+ }
102+
103+ shouldReportInvocation ( stack : string ) : boolean {
104+ return (
105+ this . _reportInvocation && ! this . _ignoredInvocationRules . matches ( stack )
106+ ) ;
107+ }
108+ }
109+
110+ const config = new CodeInjectionConfigImpl ( ) ;
111+ bugDetectorConfigurations . set ( "code-injection" , config ) ;
112+
113+ // The canary name is target-specific: Jest VM contexts and globalThis can have
114+ // different existing properties. Weak keys avoid retaining old VM contexts.
35115const canaryCache = new WeakMap < object , string > ( ) ;
36116
37117// Canary access findings are delayed until the fuzz input finishes so a later
@@ -113,18 +193,35 @@ function ensureActiveCanary(): string {
113193function createCanaryDescriptor ( canaryName : string ) : PropertyDescriptor {
114194 return {
115195 get ( ) {
116- const pendingAccess = { canaryName, invoked : false } ;
117- pendingAccesses . push ( pendingAccess ) ;
196+ const accessStack = captureStack ( ) ;
197+ const pendingAccess = config . shouldReportAccess ( accessStack )
198+ ? {
199+ canaryName,
200+ stack : accessStack ,
201+ invoked : false ,
202+ }
203+ : undefined ;
204+ if ( pendingAccess ) {
205+ pendingAccesses . push ( pendingAccess ) ;
206+ }
118207
119208 return function canaryCall ( ) {
120- pendingAccess . invoked = true ;
121- reportAndThrowFinding (
122- buildFindingMessage (
123- "Confirmed Code Injection (Canary Invoked)" ,
124- `invoked canary: ${ canaryName } ` ,
125- ) ,
126- false ,
127- ) ;
209+ const invocationStack = captureStack ( ) ;
210+ if ( config . shouldReportInvocation ( invocationStack ) ) {
211+ if ( pendingAccess ) {
212+ pendingAccess . invoked = true ;
213+ }
214+ reportAndThrowFinding (
215+ buildFindingMessage (
216+ "Confirmed Code Injection (Canary Invoked)" ,
217+ `invoked canary: ${ canaryName } ` ,
218+ invocationStack ,
219+ "ignoreInvocation" ,
220+ "If this execution sink is expected in your test environment, suppress it:" ,
221+ ) ,
222+ false ,
223+ ) ;
224+ }
128225 } ;
129226 } ,
130227 enumerable : false ,
@@ -141,12 +238,33 @@ function flushPendingAccesses(): void {
141238 buildFindingMessage (
142239 "Potential Code Injection (Canary Accessed)" ,
143240 `accessed canary: ${ pendingAccess . canaryName } ` ,
241+ pendingAccess . stack ,
242+ "ignoreAccess" ,
243+ "If this is a safe heuristic read, suppress it to continue fuzzing for code execution. Add this to your custom hooks:" ,
144244 ) ,
145245 false ,
146246 ) ;
147247 }
148248}
149249
150- function buildFindingMessage ( title : string , action : string ) : string {
151- return `${ title } -- ${ action } ` ;
250+ function buildFindingMessage (
251+ title : string ,
252+ action : string ,
253+ stack : string ,
254+ suppressionMethod : "ignoreAccess" | "ignoreInvocation" ,
255+ hint : string ,
256+ ) : string {
257+ const relevantStackLines = getUserFacingStackLines ( stack ) ;
258+ const message = [ `${ title } -- ${ action } ` ] ;
259+ if ( relevantStackLines . length > 0 ) {
260+ message . push ( ...relevantStackLines ) ;
261+ }
262+ message . push (
263+ "" ,
264+ `[!] ${ hint } ` ,
265+ " Example only: copy/paste it and adapt `stackPattern` to your needs." ,
266+ "" ,
267+ buildGenericSuppressionSnippet ( "code-injection" , suppressionMethod ) ,
268+ ) ;
269+ return message . join ( "\n" ) ;
152270}
0 commit comments