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
3+
14package scan
25
36import (
@@ -13,6 +16,10 @@ import (
1316 "github.com/projectdiscovery/gologger"
1417)
1518
19+ // GetChromeOptions takes as input the runner settings and returns
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.
1623func GetChromeOptions (r * Runner ) []func (* chromedp.ExecAllocator ) {
1724 copts := append (chromedp .DefaultExecAllocatorOptions [:],
1825 chromedp .Flag ("ignore-certificate-errors" , true ),
@@ -26,10 +33,20 @@ func GetChromeOptions(r *Runner) []func(*chromedp.ExecAllocator) {
2633 return copts
2734}
2835
36+ // GetChromeBrowser takes as input the chrome options and returns
37+ // the contexts with the associated cancel functions to use the
38+ // headless chrome browser it creates.
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.
2944func GetChromeBrowser (copts []func (* chromedp.ExecAllocator )) (context.CancelFunc , context.Context , context.CancelFunc ) {
3045 ectx , ecancel := chromedp .NewExecAllocator (context .Background (), copts ... )
3146 pctx , pcancel := chromedp .NewContext (ectx )
3247
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.
3350 if err := chromedp .Run (pctx ); err != nil {
3451 ecancel ()
3552 gologger .Fatal ().Msgf ("error starting browser: %s" , err .Error ())
@@ -38,6 +55,10 @@ func GetChromeBrowser(copts []func(*chromedp.ExecAllocator)) (context.CancelFunc
3855 return ecancel , pctx , pcancel
3956}
4057
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.
4162func buildHeaders (headers map [string ]interface {}) chromedp.Tasks {
4263 if headers == nil {
4364 return nil
@@ -46,6 +67,18 @@ func buildHeaders(headers map[string]interface{}) chromedp.Tasks {
4667 return chromedp.Tasks {network .SetExtraHTTPHeaders (network .Headers (headers ))}
4768}
4869
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.
4982func Scan (
5083 pctx context.Context ,
5184 r * Runner ,
@@ -57,31 +90,47 @@ func Scan(
5790 resDetection []string
5891 )
5992
93+ // Initialize result with the original input value and the constructed scan URL.
6094 resultData := output.ResultData {
6195 TargetURL : value ,
6296 ScanURL : targetURL ,
6397 }
6498
99+ // Wrap the parent context with a per-scan timeout so hung pages
100+ // don't block the scanner indefinitely.
65101 ctx , ctxCancel := context .WithTimeout (pctx , time .Second * time .Duration (r .Options .Timeout ))
66102 defer ctxCancel ()
67103
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.
68108 tabCtx , tabCancel := chromedp .NewContext (ctx )
69109 defer tabCancel ()
70110
111+ // Build the scan task list: optionally inject custom HTTP headers,
112+ // navigate to the target, then evaluate the prototype pollution JS payload.
71113 scanTasks := buildHeaders (headers )
72114 scanTasks = append (
73115 scanTasks ,
74116 chromedp .Navigate (targetURL ),
75117 chromedp .EvaluateAsDevTools (js , & resScan ),
76118 )
77119
120+ // Execute the scan tasks inside the dedicated tab context.
78121 errScan := chromedp .Run (tabCtx , scanTasks )
79122 if errScan != nil {
80123 resultData .ScanError = errScan .Error ()
81124 }
82125
126+ // Trim and store the JS evaluation result.
127+ // This value is reused in the exploit gate below to avoid a redundant TrimSpace call.
83128 resultData .JSEvaluation = strings .TrimSpace (resScan )
84129
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).
85134 if ! r .Options .Exploit || errScan != nil || resultData .JSEvaluation == "" {
86135 return resultData , nil
87136 }
@@ -90,23 +139,32 @@ func Scan(
90139 gologger .Info ().Label ("VULN" ).Msg (fmt .Sprintf ("Target is Vulnerable %s" , targetURL ))
91140 }
92141
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.
93146 fingerprintTasks := chromedp.Tasks {
94147 chromedp .EvaluateAsDevTools (exploit .Fingerprint , & resDetection ),
95148 }
96149
97150 errDetection := chromedp .Run (tabCtx , fingerprintTasks )
98151 if errDetection != nil {
152+ // Log detection errors unconditionally - errors are not verbosity-dependent.
99153 gologger .Error ().Msg (errDetection .Error ())
100154 resultData .FingerprintError = errDetection .Error ()
101155 }
102156
157+ // Store fingerprint results and cross-reference known exploit references.
103158 resultData .Fingerprint = resDetection
104159 resultData .References = exploit .GetReferences (resDetection )
105160
106161 if r .Options .Verbose {
107162 gologger .Info ().Msg (fmt .Sprintf ("Trying to exploit %s" , value ))
108163 }
109164
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.
110168 exploitTasks := buildHeaders (headers )
111169
112170 result , errExploit := exploit .CheckExploit (
@@ -121,6 +179,8 @@ func Scan(
121179 resultData .ExploitURLs = result
122180
123181 if errExploit != nil {
182+ // Previously this field was incorrectly set to errDetection.Error(),
183+ // masking the actual exploit error. Now correctly uses errExploit.
124184 resultData .ExploitError = errExploit .Error ()
125185 gologger .Error ().Msg (errExploit .Error ())
126186 }
0 commit comments