From ed6e677640a3f57645aa7dfd5889275f7f5304b1 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 20 Oct 2026 11:40:53 +0000 Subject: [PATCH] fix(gosec): tighten file modes and annotate false positives Write job state, configs and imported models with mode 0600, clean the paths that are read from disk, and mark the route tables, the header name and the retry jitter as reviewed false positives. Assisted-by: Claude Code:claude-sonnet-5-5 gosec Signed-off-by: Ettore Di Giacinto --- core/application/application.go | 1 + core/application/startup.go | 2 +- core/cli/run.go | 2 +- core/http/endpoints/localai/finetune.go | 2 +- core/http/routes/localai.go | 2 ++ core/services/addons/pins/store_file.go | 2 +- core/services/finetune/service.go | 8 ++++---- core/services/quantization/service.go | 10 +++++----- pkg/addons/contract/hostops.go | 1 + 9 files changed, 17 insertions(+), 13 deletions(-) diff --git a/core/application/application.go b/core/application/application.go index 0f69dde88..5e1d3915f 100644 --- a/core/application/application.go +++ b/core/application/application.go @@ -623,6 +623,7 @@ func (a *Application) waitForHealthyWorker() { } } // Add 0-1s jitter to prevent thundering-herd on the node registry + // #nosec G404 -- jitter only spreads retries, it is not security sensitive. jitter := time.Duration(rand.Int64N(int64(time.Second))) select { case <-a.applicationConfig.Context.Done(): diff --git a/core/application/startup.go b/core/application/startup.go index 7f214fa38..32f527724 100644 --- a/core/application/startup.go +++ b/core/application/startup.go @@ -730,7 +730,7 @@ func initializeWatchdog(application *Application, options *config.ApplicationCon // loadOrGenerateHMACSecret loads an HMAC secret from the given file path, // or generates a random 32-byte secret and persists it if the file doesn't exist. func loadOrGenerateHMACSecret(path string) (string, error) { - data, err := os.ReadFile(path) + data, err := os.ReadFile(filepath.Clean(path)) if err == nil { secret := string(data) if len(secret) >= 32 { diff --git a/core/cli/run.go b/core/cli/run.go index 6be697b72..dee32091f 100644 --- a/core/cli/run.go +++ b/core/cli/run.go @@ -379,7 +379,7 @@ func (r *RunCMD) Run(ctx *cliContext.Context) (result error) { config.WithMLXTunnelCallback(func(tunnels []string) { hostfile := filepath.Join(os.TempDir(), "localai_mlx_hostfile.json") data, _ := json.Marshal(tunnels) - os.WriteFile(hostfile, data, 0644) + os.WriteFile(hostfile, data, 0600) os.Setenv("MLX_DISTRIBUTED_HOSTFILE", hostfile) xlog.Debug("setting MLX_DISTRIBUTED_HOSTFILE", "value", hostfile, "tunnels", tunnels) }), diff --git a/core/http/endpoints/localai/finetune.go b/core/http/endpoints/localai/finetune.go index 9d04452a9..8405ad0c4 100644 --- a/core/http/endpoints/localai/finetune.go +++ b/core/http/endpoints/localai/finetune.go @@ -253,7 +253,7 @@ func DownloadExportedModelEndpoint(ftService *finetune.FineTuneService) echo.Han return nil } - f, err := os.Open(path) + f, err := os.Open(filepath.Clean(path)) if err != nil { return err } diff --git a/core/http/routes/localai.go b/core/http/routes/localai.go index 6327bd512..3eff66b5a 100644 --- a/core/http/routes/localai.go +++ b/core/http/routes/localai.go @@ -409,6 +409,7 @@ func RegisterLocalAIRoutes(router *echo.Echo, "load_status": "/api/models/:id/load-status", "load_cancel": "/api/models/:id/load-cancel", }, + // #nosec G101 -- route paths, not credentials. "ai_functions": map[string]string{ "tts": "/tts", "tts_voices": "/v1/audio/voices", @@ -429,6 +430,7 @@ func RegisterLocalAIRoutes(router *echo.Echo, "prompts": "/v1/mcp/prompts/:model", "resources": "/v1/mcp/resources/:model", }, + // #nosec G101 -- route paths, not credentials. "p2p": map[string]string{ "nodes": "/api/p2p", "token": "/api/p2p/token", diff --git a/core/services/addons/pins/store_file.go b/core/services/addons/pins/store_file.go index 4528f91fa..d0b314e74 100644 --- a/core/services/addons/pins/store_file.go +++ b/core/services/addons/pins/store_file.go @@ -31,7 +31,7 @@ func NewFileStore(path string) (*FileStore, error) { if path == "" { return s, nil } - b, err := os.ReadFile(path) + b, err := os.ReadFile(filepath.Clean(path)) if errors.Is(err, os.ErrNotExist) { return s, nil } diff --git a/core/services/finetune/service.go b/core/services/finetune/service.go index 31383e621..3ee9d75c4 100644 --- a/core/services/finetune/service.go +++ b/core/services/finetune/service.go @@ -115,7 +115,7 @@ func (s *FineTuneService) saveJobState(job *schema.FineTuneJob) { } statePath := filepath.Join(dir, "state.json") - if err := os.WriteFile(statePath, data, 0640); err != nil { + if err := os.WriteFile(statePath, data, 0600); err != nil { xlog.Error("Failed to write job state", "job_id", job.ID, "error", err) } } @@ -137,7 +137,7 @@ func (s *FineTuneService) loadJobsFromDisk(_ context.Context) ([]*schema.FineTun continue } statePath := filepath.Join(baseDir, entry.Name(), "state.json") - data, err := os.ReadFile(statePath) + data, err := os.ReadFile(filepath.Clean(statePath)) if err != nil { continue } @@ -660,7 +660,7 @@ func (s *FineTuneService) ExportModel(ctx context.Context, userID, jobID string, s.setExportFailed(job, fmt.Sprintf("failed to marshal config: %v", err)) return } - if err := os.WriteFile(configPath, yamlData, 0644); err != nil { + if err := os.WriteFile(configPath, yamlData, 0600); err != nil { s.setExportFailed(job, fmt.Sprintf("failed to write config file: %v", err)) return } @@ -765,7 +765,7 @@ func (s *FineTuneService) UploadDataset(filename string, data []byte) (string, e return "", fmt.Errorf("invalid filename") } filePath := filepath.Join(uploadDir, uuid.New().String()[:8]+"-"+base) - if err := os.WriteFile(filePath, data, 0640); err != nil { + if err := os.WriteFile(filePath, data, 0600); err != nil { return "", fmt.Errorf("failed to write dataset: %w", err) } diff --git a/core/services/quantization/service.go b/core/services/quantization/service.go index e4d6d78c4..e482b1216 100644 --- a/core/services/quantization/service.go +++ b/core/services/quantization/service.go @@ -138,7 +138,7 @@ func (s *QuantizationService) saveJobState(job *schema.QuantizationJob) { } statePath := filepath.Join(dir, "state.json") - if err := os.WriteFile(statePath, data, 0640); err != nil { + if err := os.WriteFile(statePath, data, 0600); err != nil { xlog.Error("Failed to write quantization job state", "job_id", job.ID, "error", err) } } @@ -160,7 +160,7 @@ func (s *QuantizationService) loadJobsFromDisk(_ context.Context) ([]*schema.Qua continue } statePath := filepath.Join(baseDir, entry.Name(), "state.json") - data, err := os.ReadFile(statePath) + data, err := os.ReadFile(filepath.Clean(statePath)) if err != nil { continue } @@ -714,12 +714,12 @@ func (s *QuantizationService) ImportModel(ctx context.Context, userID, jobID str srcFile := job.OutputFile dstFile := filepath.Join(outputPath, filepath.Base(srcFile)) - srcData, err := os.ReadFile(srcFile) + srcData, err := os.ReadFile(filepath.Clean(srcFile)) if err != nil { s.setImportFailed(job, fmt.Sprintf("failed to read output file: %v", err)) return } - if err := os.WriteFile(dstFile, srcData, 0644); err != nil { + if err := os.WriteFile(dstFile, srcData, 0600); err != nil { s.setImportFailed(job, fmt.Sprintf("failed to write model file: %v", err)) return } @@ -752,7 +752,7 @@ func (s *QuantizationService) ImportModel(ctx context.Context, userID, jobID str s.setImportFailed(job, fmt.Sprintf("failed to marshal config: %v", err)) return } - if err := os.WriteFile(configPath, yamlData, 0644); err != nil { + if err := os.WriteFile(configPath, yamlData, 0600); err != nil { s.setImportFailed(job, fmt.Sprintf("failed to write config file: %v", err)) return } diff --git a/pkg/addons/contract/hostops.go b/pkg/addons/contract/hostops.go index 5ccf4a22f..93a42d3b1 100644 --- a/pkg/addons/contract/hostops.go +++ b/pkg/addons/contract/hostops.go @@ -48,6 +48,7 @@ const ( ) // HostCredentialHeader carries the host credential of the incarnation. +// #nosec G101 -- this is a header name, not a credential value. const HostCredentialHeader = "X-Addon-Host-Credential" // hostOpName is the brief's shape ^[a-z]+\.[a-z.]+$ without an empty word: