From cf5846a6ab0f331b9908fd330bd9a1e8128243c4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:16:20 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDIUM]=20?= =?UTF-8?q?Add=20ReadHeaderTimeout=20to=20http.Server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added `ReadHeaderTimeout: 10 * time.Second` to all `http.Server` instances in the codebase. This prevents Slowloris attacks (CWE-400) and resolves `gosec G112` warnings. Added a Sentinel journal entry in `.jules/sentinel.md` documenting this security learning. Co-authored-by: euxaristia <25621994+euxaristia@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ internal/mcp/oauth.go | 1 + internal/oauth/loopback.go | 5 +++- internal/provideroauth/openrouter.go | 36 +++++++++++++++------------- internal/tools/bash_tool_test.go | 1 + 5 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 000000000..40f9d739b --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-08-07 - Add ReadHeaderTimeout to Prevent Slowloris Attacks +**Vulnerability:** Found multiple `http.Server` instances without a configured `ReadHeaderTimeout` in `internal/mcp/oauth.go`, `internal/oauth/loopback.go`, `internal/provideroauth/openrouter.go`, and `internal/tools/bash_tool_test.go`, causing gosec G112 warnings and potential Slowloris (CWE-400) vulnerability. +**Learning:** Default `http.Server` configurations do not enforce timeouts for reading request headers. This gap can be exploited by an attacker sending headers slowly to keep connections open, exhausting server resources. +**Prevention:** Always explicitly configure `ReadHeaderTimeout` (e.g., `10 * time.Second`) when initializing an `http.Server` to ensure timely request header reading. \ No newline at end of file diff --git a/internal/mcp/oauth.go b/internal/mcp/oauth.go index 678d58045..e402e5e6a 100644 --- a/internal/mcp/oauth.go +++ b/internal/mcp/oauth.go @@ -409,6 +409,7 @@ func Login(ctx context.Context, options LoginOptions) (StoredToken, error) { } resultChan := make(chan callbackResult, 1) server := &http.Server{ + ReadHeaderTimeout: 10 * time.Second, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/callback" { http.NotFound(w, r) diff --git a/internal/oauth/loopback.go b/internal/oauth/loopback.go index 6b29e0f27..463e5d5be 100644 --- a/internal/oauth/loopback.go +++ b/internal/oauth/loopback.go @@ -50,7 +50,10 @@ func NewLoopbackListenerOnPort(state string, port int) (*LoopbackListener, error state: state, result: make(chan callbackResult, 1), } - l.server = &http.Server{Handler: http.HandlerFunc(l.handle)} + l.server = &http.Server{ + ReadHeaderTimeout: 10 * time.Second, + Handler: http.HandlerFunc(l.handle), + } go func() { _ = l.server.Serve(ln) }() return l, nil } diff --git a/internal/provideroauth/openrouter.go b/internal/provideroauth/openrouter.go index 8a99b3148..36321ebf0 100644 --- a/internal/provideroauth/openrouter.go +++ b/internal/provideroauth/openrouter.go @@ -79,26 +79,28 @@ func OpenRouterLogin(ctx context.Context, opts OpenRouterOptions) (string, error codeCh := make(chan string, 1) errCh := make(chan error, 1) - server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/callback" { - http.NotFound(w, r) - return - } - if code := strings.TrimSpace(r.URL.Query().Get("code")); code != "" { - _, _ = io.WriteString(w, "OpenRouter authorization complete. You may close this window.") + server := &http.Server{ + ReadHeaderTimeout: 10 * time.Second, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/callback" { + http.NotFound(w, r) + return + } + if code := strings.TrimSpace(r.URL.Query().Get("code")); code != "" { + _, _ = io.WriteString(w, "OpenRouter authorization complete. You may close this window.") + select { + case codeCh <- code: + default: + } + return + } + w.WriteHeader(http.StatusBadRequest) + _, _ = io.WriteString(w, "Authorization failed. You may close this window.") select { - case codeCh <- code: + case errCh <- errors.New("provideroauth: callback missing authorization code"): default: } - return - } - w.WriteHeader(http.StatusBadRequest) - _, _ = io.WriteString(w, "Authorization failed. You may close this window.") - select { - case errCh <- errors.New("provideroauth: callback missing authorization code"): - default: - } - })} + })} go func() { _ = server.Serve(listener) }() defer func() { shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), time.Second) diff --git a/internal/tools/bash_tool_test.go b/internal/tools/bash_tool_test.go index 297f9816a..f10b448b5 100644 --- a/internal/tools/bash_tool_test.go +++ b/internal/tools/bash_tool_test.go @@ -69,6 +69,7 @@ func runBashToolHelper(command string) { } fmt.Println("listening", listener.Addr().String()) server := &http.Server{ + ReadHeaderTimeout: 10 * time.Second, Handler: http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { _, _ = response.Write([]byte("zero-server-ok")) }),