Skip to content

Commit b3047b5

Browse files
authored
Merge pull request #307 from 0xreacher/patch-1
Fixed Chrome Tab Leak, Wrong Error Fields & Task Mutation
2 parents 25dc019 + bc544d9 commit b3047b5

1 file changed

Lines changed: 117 additions & 62 deletions

File tree

pkg/scan/chrome.go

Lines changed: 117 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
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

74
package 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.
2423
func 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

Comments
 (0)