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
15 changes: 13 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package config
import (
"bufio"
"fmt"
"math"
"os"
"strconv"
"strings"
Expand Down Expand Up @@ -245,15 +246,25 @@ func loadBudget() (BudgetConfig, error) {
if loops, err = envInt64("RISKKERNEL_DEFAULT_LOOPS"); err != nil {
return b, err
}
b.Loops = int32(loops)
b.Loops = clampInt32(loops)
var secs int64
if secs, err = envInt64("RISKKERNEL_DEFAULT_SECONDS"); err != nil {
return b, err
}
b.Seconds = int32(secs)
b.Seconds = clampInt32(secs)
return b, nil
}

// clampInt32 narrows a non-negative int64 to int32, bounding it at math.MaxInt32
// so an out-of-range value can't silently overflow on conversion (envInt64
// already rejects negatives).
func clampInt32(v int64) int32 {
if v > math.MaxInt32 {
return math.MaxInt32
}
return int32(v)
}

func envInt64(key string) (int64, error) {
v := os.Getenv(key)
if v == "" {
Expand Down
20 changes: 20 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
package config

import (
"math"
"os"
"path/filepath"
"testing"
)

func TestLoad_BudgetClampsInt32Overflow(t *testing.T) {
withCleanEnv(t)
chdirTemp(t)
// A value beyond int32 must clamp to MaxInt32, not silently overflow/wrap.
t.Setenv("RISKKERNEL_DEFAULT_LOOPS", "5000000000") // > math.MaxInt32
t.Setenv("RISKKERNEL_DEFAULT_SECONDS", "5000000000")

cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.DefaultBudget.Loops != math.MaxInt32 {
t.Errorf("Loops = %d, want clamp to %d", cfg.DefaultBudget.Loops, int32(math.MaxInt32))
}
if cfg.DefaultBudget.Seconds != math.MaxInt32 {
t.Errorf("Seconds = %d, want clamp to %d", cfg.DefaultBudget.Seconds, int32(math.MaxInt32))
}
}

func TestLoad_Defaults(t *testing.T) {
withCleanEnv(t)
chdirTemp(t) // no .env present
Expand Down
15 changes: 13 additions & 2 deletions internal/memory/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,20 @@ func (r *Reader) resolveFile(namespace, name string) (string, error) {
return r.safeJoin(filepath.Join(namespace, name))
}

// safeJoin joins rel under the root and guarantees the result stays within it.
// safeJoin joins a user-supplied relative path under the root and guarantees the
// result cannot escape it. The cleaned path is validated with filepath.IsLocal —
// which rejects absolute paths, "..", and anything that would escape the base
// (and which static analysis recognizes as a path-traversal sanitizer) — backed
// by a prefix check as defense in depth.
func (r *Reader) safeJoin(rel string) (string, error) {
joined := filepath.Clean(filepath.Join(r.root, rel))
clean := filepath.Clean(rel)
if clean == "." { // the root itself (e.g. an empty namespace)
return r.root, nil
}
if !filepath.IsLocal(clean) {
return "", ErrUnsafePath
}
joined := filepath.Join(r.root, clean)
if joined != r.root && !strings.HasPrefix(joined, r.root+string(os.PathSeparator)) {
return "", ErrUnsafePath
}
Expand Down
Loading