1- /*
2- pphack - The Most Advanced Client-Side Prototype Pollution Scanner
3-
4- This repository is under MIT License https://github.com/edoardottt/pphack/blob/main/LICENSE
5- */
1+ // pphack - The Most Advanced Client-Side Prototype Pollution Scanner
2+ // This repository is under MIT License https://github.com/edoardottt/pphack/blob/main/LICENSE
63
74package scan
85
@@ -20,7 +17,9 @@ import (
2017)
2118
2219// GetChromeOptions takes as input the runner settings and returns
23- // the chrome options.
20+ // the chrome options used to configure the headless browser instance.
21+ // It always disables certificate errors and sets a custom user agent.
22+ // If a proxy is configured in the runner options, it is appended as well.
2423func GetChromeOptions (r * Runner ) []func (* chromedp.ExecAllocator ) {
2524 copts := append (chromedp .DefaultExecAllocatorOptions [:],
2625 chromedp .Flag ("ignore-certificate-errors" , true ),
@@ -37,97 +36,153 @@ func GetChromeOptions(r *Runner) []func(*chromedp.ExecAllocator) {
3736// GetChromeBrowser takes as input the chrome options and returns
3837// the contexts with the associated cancel functions to use the
3938// headless chrome browser it creates.
40- func GetChromeBrowser (copts []func (* chromedp.ExecAllocator )) (context.CancelFunc ,
41- context.Context , context.CancelFunc ) {
39+ // Returns ecancel (exec allocator cancel), pctx (parent browser context),
40+ // and pcancel (parent context cancel).
41+ // Callers must invoke pcancel before ecancel to ensure correct cleanup order.
42+ // ecancel is also called internally on fatal browser startup failure
43+ // to avoid leaking the exec allocator before the process exits.
44+ func GetChromeBrowser (copts []func (* chromedp.ExecAllocator )) (context.CancelFunc , context.Context , context.CancelFunc ) {
4245 ectx , ecancel := chromedp .NewExecAllocator (context .Background (), copts ... )
4346 pctx , pcancel := chromedp .NewContext (ectx )
4447
48+ // Run an empty chromedp task to verify the browser starts successfully.
49+ // If it fails, ecancel is called before Fatal to avoid leaking the allocator.
4550 if err := chromedp .Run (pctx ); err != nil {
51+ ecancel ()
4652 gologger .Fatal ().Msgf ("error starting browser: %s" , err .Error ())
4753 }
4854
4955 return ecancel , pctx , pcancel
5056}
5157
52- // Scan is the actual function that takes as input a browser context, other info
53- // and performs the scan.
54- func Scan (pctx context.Context , r * Runner , headers map [string ]interface {},
55- js , value , targetURL string ) (output.ResultData , error ) {
58+ // buildHeaders is a helper that converts a headers map into a chromedp.Tasks
59+ // slice containing the SetExtraHTTPHeaders action.
60+ // Returns nil if headers is nil, making it safe to append directly onto any
61+ // existing chromedp.Tasks without an extra nil check at the call site.
62+ func buildHeaders (headers map [string ]interface {}) chromedp.Tasks {
63+ if headers == nil {
64+ return nil
65+ }
66+
67+ return chromedp.Tasks {network .SetExtraHTTPHeaders (network .Headers (headers ))}
68+ }
69+
70+ // Scan is the core function that performs the prototype pollution scan.
71+ // It takes a parent browser context (pctx), runner config (r), optional HTTP
72+ // headers, the JavaScript payload (js), the original input value, and the
73+ // fully constructed target URL.
74+ //
75+ // Flow:
76+ // 1. Creates a timeout-scoped context and a dedicated Chrome tab context.
77+ // 2. Navigates to targetURL and evaluates the JS pollution payload.
78+ // 3. If exploit mode is enabled and the payload returned a non-empty result,
79+ // it runs fingerprinting to identify the affected library/sink.
80+ // 4. Attempts exploitation using the fingerprint results.
81+ // 5. Populates and returns a ResultData struct with all findings and errors.
82+ func Scan (
83+ pctx context.Context ,
84+ r * Runner ,
85+ headers map [string ]interface {},
86+ js , value , targetURL string ,
87+ ) (output.ResultData , error ) {
5688 var (
57- resScan string
58- resDetection []string
59- chromedpTasksScan chromedp.Tasks
60- chromedpTasksDetection chromedp.Tasks
89+ resScan string
90+ resDetection []string
6191 )
6292
93+ // Initialize result with the original input value and the constructed scan URL.
6394 resultData := output.ResultData {
6495 TargetURL : value ,
6596 ScanURL : targetURL ,
6697 }
6798
68- if headers != nil {
69- chromedpTasksScan = append (chromedpTasksScan , network .SetExtraHTTPHeaders (network .Headers (headers )))
70- }
71-
72- chromedpTasksScan = append (
73- chromedpTasksScan , chromedp .Navigate (targetURL ),
99+ // Wrap the parent context with a per-scan timeout so hung pages
100+ // don't block the scanner indefinitely.
101+ ctx , ctxCancel := context .WithTimeout (pctx , time .Second * time .Duration (r .Options .Timeout ))
102+ defer ctxCancel ()
103+
104+ // Open a new Chrome tab scoped to the timeout context.
105+ // tabCancel explicitly closes the tab when Scan returns,
106+ // preventing tab accumulation across concurrent scans.
107+ // Previously this cancel was silently dropped with _, causing a tab leak.
108+ tabCtx , tabCancel := chromedp .NewContext (ctx )
109+ defer tabCancel ()
110+
111+ // Build the scan task list: optionally inject custom HTTP headers,
112+ // navigate to the target, then evaluate the prototype pollution JS payload.
113+ scanTasks := buildHeaders (headers )
114+ scanTasks = append (
115+ scanTasks ,
116+ chromedp .Navigate (targetURL ),
74117 chromedp .EvaluateAsDevTools (js , & resScan ),
75118 )
76119
77- ctx , cancel := context .WithTimeout (pctx , time .Second * time .Duration (r .Options .Timeout ))
78- ctx , _ = chromedp .NewContext (ctx )
79-
80- defer cancel ()
81-
82- errScan := chromedp .Run (ctx , chromedpTasksScan )
120+ // Execute the scan tasks inside the dedicated tab context.
121+ errScan := chromedp .Run (tabCtx , scanTasks )
83122 if errScan != nil {
84123 resultData .ScanError = errScan .Error ()
85124 }
86125
126+ // Trim and store the JS evaluation result.
127+ // This value is reused in the exploit gate below to avoid a redundant TrimSpace call.
87128 resultData .JSEvaluation = strings .TrimSpace (resScan )
88129
89- // if I have to detect the exploit, no errors and it's vulnerable.
90- if r .Options .Exploit && errScan == nil {
91- if resTrimmed := strings .TrimSpace (resScan ); resTrimmed != "" {
92- if r .Options .Verbose {
93- gologger .Info ().Label ("VULN" ).Msg (fmt .Sprintf ("Target is Vulnerable %s" , targetURL ))
94- }
95-
96- chromedpTasksScan = append (chromedpTasksScan , chromedp .EvaluateAsDevTools (exploit .Fingerprint , & resDetection ))
97-
98- errDetection := chromedp .Run (ctx , chromedpTasksScan )
99- if errDetection != nil && r .Options .Verbose {
100- gologger .Error ().Msg (errDetection .Error ())
101- }
130+ // Early return guard: skip exploit phase entirely if:
131+ // - exploit mode is off, OR
132+ // - the scan itself errored (page unreachable, timeout, etc.), OR
133+ // - the JS payload returned empty (no pollution detected).
134+ if ! r .Options .Exploit || errScan != nil || resultData .JSEvaluation == "" {
135+ return resultData , nil
136+ }
102137
103- resultData .Fingerprint = resDetection
104- resultData .References = exploit .GetReferences (resDetection )
138+ if r .Options .Verbose {
139+ gologger .Info ().Label ("VULN" ).Msg (fmt .Sprintf ("Target is Vulnerable %s" , targetURL ))
140+ }
105141
106- if errDetection != nil {
107- resultData .FingerprintError = errDetection .Error ()
108- }
142+ // Run fingerprinting as a separate, isolated task list.
143+ // Previously the fingerprint eval was appended onto scanTasks, which caused
144+ // the full task list (Navigate + JS eval + fingerprint) to re-run from scratch,
145+ // re-navigating the page unnecessarily and potentially corrupting scan state.
146+ fingerprintTasks := chromedp.Tasks {
147+ chromedp .EvaluateAsDevTools (exploit .Fingerprint , & resDetection ),
148+ }
109149
110- if headers != nil {
111- chromedpTasksDetection = append (chromedpTasksDetection , network .SetExtraHTTPHeaders (network .Headers (headers )))
112- }
150+ errDetection := chromedp .Run (tabCtx , fingerprintTasks )
151+ if errDetection != nil {
152+ // Log detection errors unconditionally - errors are not verbosity-dependent.
153+ gologger .Error ().Msg (errDetection .Error ())
154+ resultData .FingerprintError = errDetection .Error ()
155+ }
113156
114- if r . Options . Verbose {
115- gologger . Info (). Msg ( fmt . Sprintf ( "Trying to exploit %s" , value ))
116- }
157+ // Store fingerprint results and cross-reference known exploit references.
158+ resultData . Fingerprint = resDetection
159+ resultData . References = exploit . GetReferences ( resDetection )
117160
118- result , errExploit := exploit .CheckExploit (pctx , chromedpTasksDetection , resDetection , targetURL ,
119- r .Options .Verbose , r .Options .Timeout )
161+ if r .Options .Verbose {
162+ gologger .Info ().Msg (fmt .Sprintf ("Trying to exploit %s" , value ))
163+ }
120164
121- resultData .ExploitURLs = result
165+ // Build exploit-phase headers separately using buildHeaders.
166+ // Previously this was a duplicated inline block; now it uses the shared helper
167+ // for consistency with the scan phase header handling.
168+ exploitTasks := buildHeaders (headers )
169+
170+ result , errExploit := exploit .CheckExploit (
171+ pctx ,
172+ exploitTasks ,
173+ resDetection ,
174+ targetURL ,
175+ r .Options .Verbose ,
176+ r .Options .Timeout ,
177+ )
122178
123- if errExploit != nil {
124- resultData .ExploitError = errDetection .Error ()
125- }
179+ resultData .ExploitURLs = result
126180
127- if errExploit != nil && ! r .Options .Verbose {
128- gologger .Error ().Msg (errExploit .Error ())
129- }
130- }
181+ if errExploit != nil {
182+ // Previously this field was incorrectly set to errDetection.Error(),
183+ // masking the actual exploit error. Now correctly uses errExploit.
184+ resultData .ExploitError = errExploit .Error ()
185+ gologger .Error ().Msg (errExploit .Error ())
131186 }
132187
133188 return resultData , nil
0 commit comments