mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-29 09:35:02 -04:00
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 <mudler@localai.io>
This commit is contained in:
1 parent
14c692098e
commit
53ce024efc
6 files changed
+40
-6
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)))
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user