GitHub Security Advisory: GHSA-w5pc-m664-r62v
Package: github.com/pinchtab/pinchtab (GO)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-94: Improper Control of Generation of Code ('Code Injection'), CWE-284: Improper Access Control, CWE-693: Protection Mechanism Failure
Vulnerable versions: >= 0.8.3
Patched in: unpatched

Summary: A PinchTab Security Policy Bypass in /wait Allows Arbitrary JavaScript Execution

Details:
### Summary
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.

### Details
**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:

```go
// 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`:

```go
// 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`:

```go
// 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 w