Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a8c8000
codescan(l1): batch1 lane 1 fixes for 5 alerts
KooshaPari Feb 23, 2026
aed2848
codescan(l2): batch1 lane 2 fixes for 5 alerts
KooshaPari Feb 23, 2026
d43d423
codescan(l3): batch1 lane 3 fixes for 5 alerts
KooshaPari Feb 23, 2026
000b6be
codescan(l4): batch1 lane 4 fixes for 5 alerts
KooshaPari Feb 23, 2026
94cb1a2
codescan(l5): batch1 lane 5 fixes for 5 alerts
KooshaPari Feb 23, 2026
6b52c52
codescan(l6): batch1 lane 6 fixes for 5 alerts
KooshaPari Feb 23, 2026
58ed448
codescan(b2-l1): harden request and path input validation
KooshaPari Feb 23, 2026
392c713
codescan(b2-l2): block auth path traversal in stores
KooshaPari Feb 23, 2026
be2c382
codescan(b2-l3): harden auth file save paths
KooshaPari Feb 23, 2026
9aa3b6a
codescan(b2-l4): harden auth file path handling against traversal
KooshaPari Feb 23, 2026
c2c9b61
codescan(b2-l5): fix overflow and clear-text logging findings
KooshaPari Feb 23, 2026
0f0b1c3
codescan(b2-l6): redact thinking clear-text logs
KooshaPari Feb 23, 2026
4a6eafc
codescan(b3-l1): batch3 lane 1 remediation set
KooshaPari Feb 23, 2026
53809c1
codescan(b3-l2): batch3 lane 2 remediation set
KooshaPari Feb 23, 2026
d7ab111
codescan(b3-l3): batch3 lane 3 remediation set
KooshaPari Feb 23, 2026
240842a
codescan(b3-l4): batch3 lane 4 remediation set
KooshaPari Feb 23, 2026
eb076eb
codescan(b3-l5): batch3 lane 5 remediation set
KooshaPari Feb 23, 2026
0a40ce2
codescan(b3-l6): batch3 lane 6 remediation set
KooshaPari Feb 23, 2026
d2f99ca
codescan(merge): relax path guard test assertion wording
KooshaPari Feb 23, 2026
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
51 changes: 47 additions & 4 deletions pkg/llmproxy/api/handlers/management/api_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ func (h *Handler) APICall(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid url"})
return
}
if errValidateURL := validateAPICallURL(parsedURL); errValidateURL != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": errValidateURL.Error()})
return
}

authIndex := firstNonEmptyString(body.AuthIndexSnake, body.AuthIndexCamel, body.AuthIndexPascal)
auth := h.authByIndex(authIndex)
Expand Down Expand Up @@ -294,6 +298,29 @@ func firstNonEmptyString(values ...*string) string {
return ""
}

func validateAPICallURL(parsedURL *url.URL) error {
if parsedURL == nil {
return fmt.Errorf("invalid url")
}
scheme := strings.ToLower(strings.TrimSpace(parsedURL.Scheme))
if scheme != "http" && scheme != "https" {
return fmt.Errorf("unsupported url scheme")
}
host := strings.TrimSpace(parsedURL.Hostname())
if host == "" {
return fmt.Errorf("invalid url host")
}
if strings.EqualFold(host, "localhost") {
return fmt.Errorf("target host is not allowed")
}
if ip := net.ParseIP(host); ip != nil {
if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() || ip.IsMulticast() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return fmt.Errorf("target host is not allowed")
}
}
return nil
}

func tokenValueForAuth(auth *coreauth.Auth) string {
if auth == nil {
return ""
Expand Down Expand Up @@ -1179,12 +1206,11 @@ func (h *Handler) enrichCopilotTokenResponse(ctx context.Context, response apiCa

// Fetch quota information from /copilot_pkg/llmproxy/user
// Derive the base URL from the original token request to support proxies and test servers
parsedURL, errParse := url.Parse(originalURL)
if errParse != nil {
log.WithError(errParse).Debug("enrichCopilotTokenResponse: failed to parse URL")
quotaURL, errQuotaURL := copilotQuotaURLFromTokenURL(originalURL)
if errQuotaURL != nil {
log.WithError(errQuotaURL).Debug("enrichCopilotTokenResponse: rejected token URL for quota request")
return response
}
quotaURL := fmt.Sprintf("%s://%s/copilot_pkg/llmproxy/user", parsedURL.Scheme, parsedURL.Host)

req, errNewRequest := http.NewRequestWithContext(ctx, http.MethodGet, quotaURL, nil)
if errNewRequest != nil {
Expand Down Expand Up @@ -1325,3 +1351,20 @@ func (h *Handler) enrichCopilotTokenResponse(ctx context.Context, response apiCa

return response
}

func copilotQuotaURLFromTokenURL(originalURL string) (string, error) {
parsedURL, errParse := url.Parse(strings.TrimSpace(originalURL))
if errParse != nil {
return "", errParse
}
host := strings.ToLower(parsedURL.Hostname())
if parsedURL.Scheme != "https" {
return "", fmt.Errorf("unsupported scheme %q", parsedURL.Scheme)
}
switch host {
case "api.github.com", "api.githubcopilot.com":
return fmt.Sprintf("https://%s/copilot_pkg/llmproxy/user", host), nil
default:
return "", fmt.Errorf("unsupported host %q", parsedURL.Hostname())
}
}
74 changes: 74 additions & 0 deletions pkg/llmproxy/api/handlers/management/api_tools_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package management

import (
"bytes"
"context"
"encoding/json"
"io"
Expand All @@ -17,6 +18,26 @@ import (
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
)

func TestAPICall_RejectsUnsafeHost(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)

body := []byte(`{"method":"GET","url":"http://127.0.0.1:8080/ping"}`)
req := httptest.NewRequest(http.MethodPost, "/v0/management/api-call", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")

rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = req

h := &Handler{}
h.APICall(c)

if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusBadRequest, rec.Body.String())
}
}

type memoryAuthStore struct {
mu sync.Mutex
items map[string]*coreauth.Auth
Expand Down Expand Up @@ -303,3 +324,56 @@ func TestGetKiroQuotaWithChecker_MissingProfileARN(t *testing.T) {
t.Fatalf("unexpected response body: %s", rec.Body.String())
}
}

func TestCopilotQuotaURLFromTokenURL(t *testing.T) {
t.Parallel()

tests := []struct {
name string
tokenURL string
wantURL string
expectErr bool
}{
{
name: "github_api",
tokenURL: "https://api.github.com/copilot_internal/v2/token",
wantURL: "https://api.github.com/copilot_pkg/llmproxy/user",
expectErr: false,
},
{
name: "copilot_api",
tokenURL: "https://api.githubcopilot.com/copilot_internal/v2/token",
wantURL: "https://api.githubcopilot.com/copilot_pkg/llmproxy/user",
expectErr: false,
},
{
name: "reject_http",
tokenURL: "http://api.github.com/copilot_internal/v2/token",
expectErr: true,
},
{
name: "reject_untrusted_host",
tokenURL: "https://127.0.0.1/copilot_internal/v2/token",
expectErr: true,
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got, err := copilotQuotaURLFromTokenURL(tt.tokenURL)
if tt.expectErr {
if err == nil {
t.Fatalf("expected error, got url=%q", got)
}
return
}
if err != nil {
t.Fatalf("copilotQuotaURLFromTokenURL returned error: %v", err)
}
if got != tt.wantURL {
t.Fatalf("copilotQuotaURLFromTokenURL = %q, want %q", got, tt.wantURL)
}
})
}
}
Loading
Loading