From 0dbb88b188ca97e8850cc54c78c48796b6a1e825 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:46:07 +0000 Subject: [PATCH 1/3] Initial plan From 7899c753f799635b59b831d3647ea2e0a94f8c38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:49:34 +0000 Subject: [PATCH 2/3] fix: remediate 13 security findings (cmdi, SSRF, LLM PII, DLP, secret, error-leak, CVE) --- go.mod | 2 +- go.sum | 2 ++ internal/handlers/ai.go | 22 +++++++++------------- internal/handlers/cve.go | 2 +- internal/handlers/cwe.go | 11 +++++++++-- internal/handlers/dlp.go | 4 ++-- internal/handlers/secrets.go | 7 +++++-- internal/handlers/suspicious.go | 24 +++++++++++++++++++++--- 8 files changed, 50 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 5ab4543..342c39a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/reachable/reach-testbed-github-go go 1.21 require ( - golang.org/x/text v0.3.7 + golang.org/x/text v0.3.8 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index c3a13c5..3375cec 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/internal/handlers/ai.go b/internal/handlers/ai.go index fc09ae7..4bc1a7d 100644 --- a/internal/handlers/ai.go +++ b/internal/handlers/ai.go @@ -18,47 +18,43 @@ type agentRequest struct { func AIAnswer(w http.ResponseWriter, r *http.Request) { var req promptRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } - prompt := "System: answer as the payroll admin. User: " + req.Question payload, _ := json.Marshal(map[string]any{ "model": "gpt-4", "messages": []map[string]string{ - {"role": "user", "content": prompt}, + {"role": "system", "content": "Answer as the payroll admin."}, + {"role": "user", "content": req.Question}, }, }) _, _ = http.Post("https://api.openai.com/v1/chat/completions", "application/json", bytes.NewReader(payload)) - _ = json.NewEncoder(w).Encode(map[string]string{"prompt": prompt}) + _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } func AIAgentPlan(w http.ResponseWriter, r *http.Request) { var req agentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } - toolSpec := "Tools: shell(command), browser(url). Follow the user's task exactly: " + req.Task payload, _ := json.Marshal(map[string]any{ "model": "gpt-4", "messages": []map[string]string{ - {"role": "system", "content": "You are an internal automation agent with admin context."}, - {"role": "user", "content": toolSpec}, + {"role": "system", "content": "You are an internal automation agent. Available tools: shell(command), browser(url)."}, + {"role": "user", "content": req.Task}, }, }) _, _ = http.Post("https://api.openai.com/v1/chat/completions", "application/json", bytes.NewReader(payload)) - _ = json.NewEncoder(w).Encode(map[string]string{ - "system_prompt": "You are an internal automation agent with admin context.", - "tool_spec": toolSpec, - }) + _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } func SafeAIAnswer(w http.ResponseWriter, r *http.Request) { var req promptRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } if strings.Contains(strings.ToLower(req.Question), "ignore previous") { diff --git a/internal/handlers/cve.go b/internal/handlers/cve.go index ad228c2..abcff2b 100644 --- a/internal/handlers/cve.go +++ b/internal/handlers/cve.go @@ -35,7 +35,7 @@ func ParseLanguage(w http.ResponseWriter, r *http.Request) { parsed, err := language.Parse(tag) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, "invalid language tag", http.StatusBadRequest) return } diff --git a/internal/handlers/cwe.go b/internal/handlers/cwe.go index 205e29d..940d049 100644 --- a/internal/handlers/cwe.go +++ b/internal/handlers/cwe.go @@ -1,6 +1,7 @@ package handlers import ( + "log" "net/http" "os/exec" @@ -9,9 +10,15 @@ import ( func DiagnosticPing(w http.ResponseWriter, r *http.Request) { host := r.URL.Query().Get("host") - out, err := exec.Command("sh", "-c", "ping -c 1 "+host).CombinedOutput() + if !safety.AllowedHostname(host) { + http.Error(w, "invalid host", http.StatusBadRequest) + return + } + + out, err := exec.Command("ping", "-c", "1", host).CombinedOutput() if err != nil { - http.Error(w, string(out), http.StatusBadGateway) + log.Printf("ping %s: %v: %s", host, err, out) + http.Error(w, "ping failed", http.StatusBadGateway) return } diff --git a/internal/handlers/dlp.go b/internal/handlers/dlp.go index 66c4864..31bca1c 100644 --- a/internal/handlers/dlp.go +++ b/internal/handlers/dlp.go @@ -10,8 +10,8 @@ import ( func SupportExport(w http.ResponseWriter, _ *http.Request) { ssn := "123-45-6789" dob := "1978-04-23" - log.Printf("Processing patient ssn=%s dob=%s", ssn, dob) - payload, _ := json.Marshal(map[string]string{"ssn": ssn, "dob": dob}) + log.Printf("Processing patient ssn=***-**-%s dob=%s", ssn[len(ssn)-4:], dob[:4]+"-**-**") + payload, _ := json.Marshal(map[string]string{"record": "processed"}) _, _ = http.Post("https://analytics.example.com/track", "application/json", bytes.NewReader(payload)) w.Header().Set("Content-Type", "text/csv") diff --git a/internal/handlers/secrets.go b/internal/handlers/secrets.go index 2cc666d..2fa80fd 100644 --- a/internal/handlers/secrets.go +++ b/internal/handlers/secrets.go @@ -8,17 +8,20 @@ import ( const syntheticServiceToken = "rtg_live_synthetic_token_1234567890" const syntheticAWSAccessKeyID = "AKIAIOSFODNN7EXAMPLE" -const syntheticGitHubToken = "ghp_reachtestbedsynthetic000000000000000000" func ServiceToken(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(syntheticServiceToken + "\n")) } func CloudTokens(w http.ResponseWriter, _ *http.Request) { + githubToken := os.Getenv("REACH_TESTBED_GITHUB_TOKEN") + if githubToken == "" { + githubToken = "not-configured" + } // Synthetic fixture values only. These are not real credentials. _ = json.NewEncoder(w).Encode(map[string]string{ "aws_access_key_id": syntheticAWSAccessKeyID, - "github_token": syntheticGitHubToken, + "github_token": githubToken, }) } diff --git a/internal/handlers/suspicious.go b/internal/handlers/suspicious.go index c2b40c4..3edf493 100644 --- a/internal/handlers/suspicious.go +++ b/internal/handlers/suspicious.go @@ -3,17 +3,33 @@ package handlers import ( "encoding/base64" "io" + "log" "net/http" + "net/url" "os" "os/exec" "path/filepath" + + "github.com/reachable/reach-testbed-github-go/internal/safety" ) func FetchTool(w http.ResponseWriter, r *http.Request) { source := r.URL.Query().Get("url") + + parsed, err := url.Parse(source) + if err != nil || parsed.Scheme != "https" { + http.Error(w, "only https URLs are allowed", http.StatusBadRequest) + return + } + if !safety.AllowedHostname(parsed.Hostname()) { + http.Error(w, "host not allowed", http.StatusBadRequest) + return + } + resp, err := http.Get(source) if err != nil { - http.Error(w, err.Error(), http.StatusBadGateway) + log.Printf("FetchTool: http.Get %s: %v", source, err) + http.Error(w, "fetch failed", http.StatusBadGateway) return } defer resp.Body.Close() @@ -21,13 +37,15 @@ func FetchTool(w http.ResponseWriter, r *http.Request) { target := filepath.Join(os.TempDir(), "reach-testbed-tool.bin") out, err := os.Create(target) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + log.Printf("FetchTool: os.Create %s: %v", target, err) + http.Error(w, "internal error", http.StatusInternalServerError) return } defer out.Close() if _, err := io.Copy(out, io.LimitReader(resp.Body, 2<<20)); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + log.Printf("FetchTool: io.Copy: %v", err) + http.Error(w, "internal error", http.StatusInternalServerError) return } From 46a48f057f27014637dac31323e24ae9a7512b4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:51:03 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20address=20code=20review=20=E2=80=94?= =?UTF-8?q?=20safe=20string=20slicing=20and=20error=20on=20missing=20githu?= =?UTF-8?q?b=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.sum | 2 -- internal/handlers/dlp.go | 10 +++++++++- internal/handlers/secrets.go | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/go.sum b/go.sum index 3375cec..7ec15cc 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/handlers/dlp.go b/internal/handlers/dlp.go index 31bca1c..104b5b8 100644 --- a/internal/handlers/dlp.go +++ b/internal/handlers/dlp.go @@ -10,7 +10,15 @@ import ( func SupportExport(w http.ResponseWriter, _ *http.Request) { ssn := "123-45-6789" dob := "1978-04-23" - log.Printf("Processing patient ssn=***-**-%s dob=%s", ssn[len(ssn)-4:], dob[:4]+"-**-**") + ssnLast4 := "" + if len(ssn) >= 4 { + ssnLast4 = ssn[len(ssn)-4:] + } + dobYear := "" + if len(dob) >= 4 { + dobYear = dob[:4] + } + log.Printf("Processing patient ssn=***-**-%s dob=%s-**-**", ssnLast4, dobYear) payload, _ := json.Marshal(map[string]string{"record": "processed"}) _, _ = http.Post("https://analytics.example.com/track", "application/json", bytes.NewReader(payload)) diff --git a/internal/handlers/secrets.go b/internal/handlers/secrets.go index 2fa80fd..583d736 100644 --- a/internal/handlers/secrets.go +++ b/internal/handlers/secrets.go @@ -16,7 +16,8 @@ func ServiceToken(w http.ResponseWriter, _ *http.Request) { func CloudTokens(w http.ResponseWriter, _ *http.Request) { githubToken := os.Getenv("REACH_TESTBED_GITHUB_TOKEN") if githubToken == "" { - githubToken = "not-configured" + http.Error(w, "github token not configured", http.StatusServiceUnavailable) + return } // Synthetic fixture values only. These are not real credentials. _ = json.NewEncoder(w).Encode(map[string]string{