From 53ce024efcb13fb190a656b005ad19ec306d24a1 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 07:32:19 +0000 Subject: [PATCH] fix(localai-proxy): clear the gosec findings Code scanning flagged seven issues in the new backend: - G115 text.go: tool-call indexes and tokenize lengths come from the upstream server as int and were cast straight to int32. Add clampInt32 so an absurd upstream value saturates instead of wrapping. - G115 live.go: the int16 -> uint16 cast in PCM16 encoding is a deliberate two's-complement reinterpretation of an already clamped sample; mark it with #nosec and say so. - G304 proxy.go, media.go, client.go: api_key_file comes from the model config, and the media input and output paths are files core staged or chose for the call. None are caller-supplied. Clean the paths and add #nosec with that reason, as core/gallery and the sound classification endpoint already do. - G306 media.go: write generated media 0o600. Core runs as the same user and serves the file itself. gosec reports 0 issues for backend/go/localai-proxy and core/services/failover. The G104 once reported for failover/prober.go is no longer present. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Ettore Di Giacinto --- backend/go/localai-proxy/client.go | 3 ++- backend/go/localai-proxy/live.go | 1 + backend/go/localai-proxy/media.go | 8 ++++++-- backend/go/localai-proxy/proxy.go | 4 +++- backend/go/localai-proxy/text.go | 19 +++++++++++++++++-- backend/go/localai-proxy/text_test.go | 11 +++++++++++ 6 files changed, 40 insertions(+), 6 deletions(-) diff --git a/backend/go/localai-proxy/client.go b/backend/go/localai-proxy/client.go index 166fa1ab8..6b9ecfa18 100644 --- a/backend/go/localai-proxy/client.go +++ b/backend/go/localai-proxy/client.go @@ -355,7 +355,8 @@ func (p *LocalAIProxy) doToFile(req *http.Request, path, dst string) (http.Heade } defer func() { _ = resp.Body.Close() }() - f, err := os.Create(dst) + // #nosec G304 -- dst is the output path core chose for this call (generated content dir), never a caller-supplied path + f, err := os.Create(filepath.Clean(dst)) if err != nil { return nil, status.Errorf(codes.Internal, "localai-proxy: create %s: %v", dst, err) } diff --git a/backend/go/localai-proxy/live.go b/backend/go/localai-proxy/live.go index 0193b973a..8787ac0fd 100644 --- a/backend/go/localai-proxy/live.go +++ b/backend/go/localai-proxy/live.go @@ -399,6 +399,7 @@ func pcm16LE(pcm []float32) []byte { v = 0 } v = math.Max(-1, math.Min(1, v)) + // #nosec G115 -- two's-complement reinterpretation for little-endian PCM16 encoding, value range already clamped binary.LittleEndian.PutUint16(buf[i*2:], uint16(int16(v*math.MaxInt16))) } return buf diff --git a/backend/go/localai-proxy/media.go b/backend/go/localai-proxy/media.go index b4bff219d..8b598c01d 100644 --- a/backend/go/localai-proxy/media.go +++ b/backend/go/localai-proxy/media.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "os" + "path/filepath" "strconv" "strings" @@ -26,7 +27,8 @@ func fileToBase64(path string) (string, error) { if path == "" { return "", nil } - data, err := os.ReadFile(path) + // #nosec G304 -- path is a staging file core wrote for this call, never a caller-supplied path + data, err := os.ReadFile(filepath.Clean(path)) if err != nil { return "", status.Errorf(codes.InvalidArgument, "localai-proxy: read %s: %v", path, err) } @@ -82,7 +84,9 @@ func (p *LocalAIProxy) writeGenItem(ctx context.Context, path string, items []ge if err != nil { return status.Errorf(codes.Internal, "localai-proxy: decode %s b64_json: %v", path, err) } - if err := os.WriteFile(dst, data, 0o644); err != nil { + // 0o600: core runs as the same user and serves the file itself, so no + // one else needs to read generated media. + if err := os.WriteFile(dst, data, 0o600); err != nil { _ = os.Remove(dst) return status.Errorf(codes.Internal, "localai-proxy: write %s: %v", dst, err) } diff --git a/backend/go/localai-proxy/proxy.go b/backend/go/localai-proxy/proxy.go index aa205d27c..a1d58bb02 100644 --- a/backend/go/localai-proxy/proxy.go +++ b/backend/go/localai-proxy/proxy.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "os" + "path/filepath" "strings" "sync/atomic" "time" @@ -159,7 +160,8 @@ func resolveAPIKey(envName, filePath string) (string, error) { return v, nil } if filePath != "" { - b, err := os.ReadFile(filePath) + // #nosec G304 -- api_key_file comes from the operator's model config (passed by core as a backend option), not from a request + b, err := os.ReadFile(filepath.Clean(filePath)) if err != nil { return "", fmt.Errorf("localai-proxy: read api_key_file %q: %w", filePath, err) } diff --git a/backend/go/localai-proxy/text.go b/backend/go/localai-proxy/text.go index 3b73defa2..2c7a6a0ff 100644 --- a/backend/go/localai-proxy/text.go +++ b/backend/go/localai-proxy/text.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "encoding/json" + "math" "strings" "github.com/mudler/xlog" @@ -181,7 +182,7 @@ func replyFromChoice(c textChoice, streaming bool) *pb.Reply { delta := &pb.ChatDelta{Content: content, ReasoningContent: reasoning} for _, tc := range d.ToolCalls { delta.ToolCalls = append(delta.ToolCalls, &pb.ToolCallDelta{ - Index: int32(tc.Index), + Index: clampInt32(tc.Index), Id: tc.ID, Name: tc.Function.Name, Arguments: tc.Function.Arguments, @@ -350,7 +351,7 @@ func (p *LocalAIProxy) TokenizeString(opts *pb.PredictOptions) (pb.TokenizationR if err := p.postJSON(context.Background(), "/v1/tokenize", body, &resp); err != nil { return pb.TokenizationResponse{}, err } - return pb.TokenizationResponse{Length: int32(len(resp.Tokens)), Tokens: resp.Tokens}, nil + return pb.TokenizationResponse{Length: clampInt32(len(resp.Tokens)), Tokens: resp.Tokens}, nil } func (p *LocalAIProxy) Detokenize(in *pb.DetokenizeRequest) (pb.DetokenizeResponse, error) { @@ -402,3 +403,17 @@ func (p *LocalAIProxy) Score(ctx context.Context, in *pb.ScoreRequest) (*pb.Scor } return out, nil } + +// clampInt32 narrows an upstream-supplied int (token counts, tool-call +// indexes) to the int32 the gRPC protocol carries. The upstream is another +// server, so an absurd value must saturate rather than wrap to a negative or +// small number that core would take at face value. +func clampInt32(n int) int32 { + switch { + case n > math.MaxInt32: + return math.MaxInt32 + case n < math.MinInt32: + return math.MinInt32 + } + return int32(n) +} diff --git a/backend/go/localai-proxy/text_test.go b/backend/go/localai-proxy/text_test.go index 5039f41bd..7887a7d38 100644 --- a/backend/go/localai-proxy/text_test.go +++ b/backend/go/localai-proxy/text_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "math" "net/http" "os" "path/filepath" @@ -548,3 +549,13 @@ var _ = Describe("localai-proxy", func() { }) }) }) + +var _ = Describe("clampInt32", func() { + It("passes in-range values through and saturates the rest", func() { + Expect(clampInt32(0)).To(Equal(int32(0))) + Expect(clampInt32(42)).To(Equal(int32(42))) + Expect(clampInt32(-7)).To(Equal(int32(-7))) + Expect(clampInt32(math.MaxInt32 + 1)).To(Equal(int32(math.MaxInt32))) + Expect(clampInt32(math.MinInt32 - 1)).To(Equal(int32(math.MinInt32))) + }) +})