Skip to content
Merged
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
62 changes: 27 additions & 35 deletions pkg/llmproxy/api/middleware/response_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ package middleware

import (
"bytes"
"crypto/sha256"
"fmt"
"net/http"
"strings"
"time"

"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/logging"
"github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/util"
)

const requestBodyOverrideContextKey = "REQUEST_BODY_OVERRIDE"
Expand Down Expand Up @@ -200,30 +201,13 @@ func (w *ResponseWriterWrapper) captureCurrentHeaders() {
w.headers = make(map[string][]string)
}

// Remove previous values to avoid stale entries after header mutation.
for key := range w.headers {
delete(w.headers, key)
}

// Capture all current headers from the underlying ResponseWriter
for key, values := range w.Header() {
if key == "" {
continue
}
keyLower := strings.ToLower(strings.TrimSpace(key))
sanitizedValues := make([]string, len(values))
for i, value := range values {
sanitizedValues[i] = sanitizeResponseHeaderValue(keyLower, value)
}
w.headers[key] = sanitizedValues
}
}

func sanitizeResponseHeaderValue(keyLower, value string) string {
if keyLower == "authorization" || keyLower == "cookie" || keyLower == "proxy-authorization" || keyLower == "set-cookie" {
return "[redacted]"
// Make a copy of the values slice to avoid reference issues
headerValues := make([]string, len(values))
copy(headerValues, values)
w.headers[key] = headerValues
}
return util.MaskSensitiveHeaderValue(keyLower, value)
}

// detectStreaming determines if a response should be treated as a streaming response.
Expand Down Expand Up @@ -355,7 +339,7 @@ func (w *ResponseWriterWrapper) extractAPIRequest(c *gin.Context) []byte {
if !ok || len(data) == 0 {
return nil
}
return sanitizeLoggedPayloadBytes(data)
return data
}

func (w *ResponseWriterWrapper) extractAPIResponse(c *gin.Context) []byte {
Expand All @@ -367,7 +351,7 @@ func (w *ResponseWriterWrapper) extractAPIResponse(c *gin.Context) []byte {
if !ok || len(data) == 0 {
return nil
}
return sanitizeLoggedPayloadBytes(data)
return data
}

func (w *ResponseWriterWrapper) extractAPIResponseTimestamp(c *gin.Context) time.Time {
Expand All @@ -387,17 +371,17 @@ func (w *ResponseWriterWrapper) extractRequestBody(c *gin.Context) []byte {
switch value := bodyOverride.(type) {
case []byte:
if len(value) > 0 {
return sanitizeLoggedPayloadBytes(value)
return bytes.Clone(value)
}
case string:
if strings.TrimSpace(value) != "" {
return sanitizeLoggedPayloadBytes([]byte(value))
return []byte(value)
}
}
}
}
if w.requestInfo != nil && len(w.requestInfo.Body) > 0 {
return sanitizeLoggedPayloadBytes(w.requestInfo.Body)
return w.requestInfo.Body
}
return nil
}
Expand All @@ -415,12 +399,12 @@ func (w *ResponseWriterWrapper) logRequest(requestBody []byte, statusCode int, h
w.requestInfo.URL,
w.requestInfo.Method,
requestHeaders,
requestBody,
redactLoggedBody(requestBody),
statusCode,
headers,
body,
apiRequestBody,
apiResponseBody,
redactLoggedBody(body),
redactLoggedBody(apiRequestBody),
redactLoggedBody(apiResponseBody),
apiResponseErrors,
forceLog,
w.requestInfo.RequestID,
Expand All @@ -433,15 +417,23 @@ func (w *ResponseWriterWrapper) logRequest(requestBody []byte, statusCode int, h
w.requestInfo.URL,
w.requestInfo.Method,
requestHeaders,
requestBody,
redactLoggedBody(requestBody),
statusCode,
headers,
body,
apiRequestBody,
apiResponseBody,
redactLoggedBody(body),
redactLoggedBody(apiRequestBody),
redactLoggedBody(apiResponseBody),
apiResponseErrors,
w.requestInfo.RequestID,
w.requestInfo.Timestamp,
apiResponseTimestamp,
)
}

func redactLoggedBody(body []byte) []byte {
if len(body) == 0 {
return nil
}
sum := sha256.Sum256(body)
return []byte(fmt.Sprintf("[redacted body len=%d sha256=%x]", len(body), sum[:8]))
}
28 changes: 25 additions & 3 deletions pkg/llmproxy/executor/codex_websockets_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"io"
"net"
Expand Down Expand Up @@ -1295,15 +1296,15 @@
}

func logCodexWebsocketConnected(sessionID string, authID string, wsURL string) {
log.Infof("codex websockets: upstream connected session=%s auth=%s url=%s", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL))
log.Infof("codex websockets: upstream connected session=%s auth=%s endpoint=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL))
}

func logCodexWebsocketDisconnected(sessionID, authID, wsURL, reason string, err error) {
if err != nil {
log.Infof("codex websockets: upstream disconnected session=%s auth=%s url=%s reason=%s err=%v", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL), strings.TrimSpace(reason), err)
log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s err=%v", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason), err)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

Sensitive data returned by an access to apiKeyModelAlias
flows to a logging call.

Copilot Autofix

AI about 2 months ago

General approach: Do not log the raw error value coming from thinking.ApplyThinking (or any error that may encapsulate sensitive model/alias info) in clear text. Instead, log only non-sensitive, high-level information: whether an error occurred, a generic reason, and possibly a safe, coarse-grained classification (e.g., error type/code) that does not contain user- or tenant-specific data.

Best targeted fix: Modify logCodexWebsocketDisconnected in pkg/llmproxy/executor/codex_websockets_executor.go so that when err is non-nil, we do not interpolate err directly with %v. Instead:

  • Detect whether err is a *thinking.ThinkingError and, if so, log only its Code (and maybe a generic message) rather than the full error string.
  • For non-thinking errors, log a generic string such as "internal_error" or just omit the error details.
  • Keep existing sanitization for sessionID, authID, and wsURL via sanitizeCodexSessionID, sanitizeCodexWebsocketLogField, and sanitizeCodexWebsocketLogEndpoint.

Concretely:

  1. Update the if err != nil block in logCodexWebsocketDisconnected:

    • Replace err=%v with err=%s and pass a redacted string returned by a new helper, e.g., sanitizeCodexError(err).
  2. Introduce a new helper function in the same file, e.g.:

    func sanitizeCodexError(err error) string {
        if err == nil {
            return ""
        }
        // Avoid leaking sensitive details; provide only coarse-grained info.
        if te, ok := err.(*thinking.ThinkingError); ok && te != nil {
            if te.Code != "" {
                return fmt.Sprintf("thinking_error(%s)", te.Code)
            }
            return "thinking_error"
        }
        return "error"
    }

    This uses only existing imports (fmt and thinking are already imported in this file).

  3. Ensure no other code changes are required: callers still pass the same err value, but the log line is now safe because it no longer prints the raw error string.

No changes are required in the other files (sdk/cliproxy/auth/conductor.go, pkg/llmproxy/registry/*.go, pkg/llmproxy/thinking/*.go) since they are not directly responsible for logging; they only participate in the dataflow.


Suggested changeset 1
pkg/llmproxy/executor/codex_websockets_executor.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/pkg/llmproxy/executor/codex_websockets_executor.go b/pkg/llmproxy/executor/codex_websockets_executor.go
--- a/pkg/llmproxy/executor/codex_websockets_executor.go
+++ b/pkg/llmproxy/executor/codex_websockets_executor.go
@@ -1301,7 +1301,7 @@
 
 func logCodexWebsocketDisconnected(sessionID, authID, wsURL, reason string, err error) {
 	if err != nil {
-		log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s err=%v", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason), err)
+		log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s err=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason), sanitizeCodexError(err))
 		return
 	}
 	log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason))
@@ -1311,6 +1311,20 @@
 	return util.HideAPIKey(strings.TrimSpace(raw))
 }
 
+func sanitizeCodexError(err error) string {
+	if err == nil {
+		return ""
+	}
+	// Avoid logging raw error details that may contain sensitive information.
+	if te, ok := err.(*thinking.ThinkingError); ok && te != nil {
+		if te.Code != "" {
+			return fmt.Sprintf("thinking_error(%s)", te.Code)
+		}
+		return "thinking_error"
+	}
+	return "error"
+}
+
 func sanitizeCodexWebsocketLogURL(raw string) string {
 	trimmed := strings.TrimSpace(raw)
 	if trimmed == "" {
EOF
@@ -1301,7 +1301,7 @@

func logCodexWebsocketDisconnected(sessionID, authID, wsURL, reason string, err error) {
if err != nil {
log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s err=%v", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason), err)
log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s err=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason), sanitizeCodexError(err))
return
}
log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason))
@@ -1311,6 +1311,20 @@
return util.HideAPIKey(strings.TrimSpace(raw))
}

func sanitizeCodexError(err error) string {
if err == nil {
return ""
}
// Avoid logging raw error details that may contain sensitive information.
if te, ok := err.(*thinking.ThinkingError); ok && te != nil {
if te.Code != "" {
return fmt.Sprintf("thinking_error(%s)", te.Code)
}
return "thinking_error"
}
return "error"
}

func sanitizeCodexWebsocketLogURL(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
Copilot is powered by AI and may make mistakes. Always verify output.
return
}
log.Infof("codex websockets: upstream disconnected session=%s auth=%s url=%s reason=%s", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL), strings.TrimSpace(reason))
log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason))
}

func sanitizeCodexWebsocketLogField(raw string) string {
Expand All @@ -1325,6 +1326,27 @@
return parsed.String()
}

func sanitizeCodexWebsocketLogEndpoint(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
parsed, err := url.Parse(trimmed)
if err != nil || parsed.Host == "" {
return "redacted-endpoint"
}
return parsed.Scheme + "://" + parsed.Host
}

func sanitizeCodexSessionID(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
sum := sha256.Sum256([]byte(trimmed))

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data High

Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.

Copilot Autofix

AI about 2 months ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

return fmt.Sprintf("sess_%x", sum[:6])
}

// CodexAutoExecutor routes Codex requests to the websocket transport only when:
// 1. The downstream transport is websocket, and
// 2. The selected auth enables websockets.
Expand Down
12 changes: 11 additions & 1 deletion pkg/llmproxy/registry/model_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import (
"context"
"crypto/sha256"
"fmt"
"sort"
"strings"
Expand Down Expand Up @@ -661,7 +662,7 @@
registration.SuspendedClients[clientID] = reason
registration.LastUpdated = time.Now()
if reason != "" {
log.Debugf("Suspended client %s for model %s (reason provided)", clientID, modelID)
log.Debugf("Suspended client %s for model %s (reason provided)", logSafeRegistryID(clientID), logSafeRegistryID(modelID))
} else {
log.Debug("Suspended client for model")
}
Expand Down Expand Up @@ -690,6 +691,15 @@
log.Debug("Resumed suspended client for model")
}

func logSafeRegistryID(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
sum := sha256.Sum256([]byte(trimmed))

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data High

Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.

Copilot Autofix

AI about 2 months ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

return fmt.Sprintf("id_%x", sum[:6])
}

// ClientSupportsModel reports whether the client registered support for modelID.
func (r *ModelRegistry) ClientSupportsModel(clientID, modelID string) bool {
clientID = strings.TrimSpace(clientID)
Expand Down
28 changes: 25 additions & 3 deletions pkg/llmproxy/runtime/executor/codex_websockets_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"io"
"net"
Expand Down Expand Up @@ -1295,15 +1296,15 @@
}

func logCodexWebsocketConnected(sessionID string, authID string, wsURL string) {
log.Infof("codex websockets: upstream connected session=%s auth=%s url=%s", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL))
log.Infof("codex websockets: upstream connected session=%s auth=%s endpoint=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL))
}

func logCodexWebsocketDisconnected(sessionID string, authID string, wsURL string, reason string, err error) {
if err != nil {
log.Infof("codex websockets: upstream disconnected session=%s auth=%s url=%s reason=%s err=%v", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL), strings.TrimSpace(reason), err)
log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s err=%v", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason), err)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

Sensitive data returned by an access to apiKeyModelAlias
flows to a logging call.

Copilot Autofix

AI about 2 months ago

In general terms, the fix is to ensure that any error value (err) that may encapsulate sensitive information (here, model IDs from API key–specific aliasing) is not logged in clear text. Instead, log only non-sensitive metadata (e.g., that an error occurred, session identifiers already sanitized, reason string) or a redacted form of the error.

In this codebase, the problematic logging happens in logCodexWebsocketDisconnected in pkg/llmproxy/runtime/executor/codex_websockets_executor.go:

if err != nil {
    log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s err=%v", ...)
    return
}

To fix this without changing external behavior of the executor, we can keep the fact that an error occurred but remove or neutralize the direct %v formatting of err. Since we are not allowed to change imports outside the shown snippet and we already have logrus imported, we can simply:

  • Change the logging to avoid printing err altogether, or
  • Replace it with a generic placeholder (e.g., "err=failed") that does not include sensitive content.

Given the project’s patterns (it already uses redaction helpers for other log fields), the most conservative and simple fix is to drop the error detail from this high-level info log and rely on other, lower-level logs to capture error specifics if needed. Concretely:

  • In logCodexWebsocketDisconnected, replace the Infof call in the err != nil branch with a version that omits err from the format string and arguments.
  • Leave the rest of the function and file unchanged; no new imports or helper methods are necessary.

This ensures that no sensitive data inside err (including possible model IDs) is logged in clear text while preserving the operational signal that a disconnection occurred and whether it was due to an error.


Suggested changeset 1
pkg/llmproxy/runtime/executor/codex_websockets_executor.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/pkg/llmproxy/runtime/executor/codex_websockets_executor.go b/pkg/llmproxy/runtime/executor/codex_websockets_executor.go
--- a/pkg/llmproxy/runtime/executor/codex_websockets_executor.go
+++ b/pkg/llmproxy/runtime/executor/codex_websockets_executor.go
@@ -1301,7 +1301,7 @@
 
 func logCodexWebsocketDisconnected(sessionID string, authID string, wsURL string, reason string, err error) {
 	if err != nil {
-		log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s err=%v", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason), err)
+		log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason))
 		return
 	}
 	log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason))
EOF
@@ -1301,7 +1301,7 @@

func logCodexWebsocketDisconnected(sessionID string, authID string, wsURL string, reason string, err error) {
if err != nil {
log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s err=%v", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason), err)
log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason))
return
}
log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason))
Copilot is powered by AI and may make mistakes. Always verify output.
return
}
log.Infof("codex websockets: upstream disconnected session=%s auth=%s url=%s reason=%s", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL), strings.TrimSpace(reason))
log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason))
}

func sanitizeCodexWebsocketLogField(raw string) string {
Expand All @@ -1325,6 +1326,27 @@
return parsed.String()
}

func sanitizeCodexWebsocketLogEndpoint(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
parsed, err := url.Parse(trimmed)
if err != nil || parsed.Host == "" {
return "redacted-endpoint"
}
return parsed.Scheme + "://" + parsed.Host
}

func sanitizeCodexSessionID(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
sum := sha256.Sum256([]byte(trimmed))

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data High

Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
return fmt.Sprintf("sess_%x", sum[:6])
}

// CodexAutoExecutor routes Codex requests to the websocket transport only when:
// 1. The downstream transport is websocket, and
// 2. The selected auth enables websockets.
Expand Down
Loading