@@ -4,13 +4,30 @@ import (
44 "bufio"
55 "context"
66 "encoding/json"
7+ "errors"
78 "fmt"
89 "os"
910 "path/filepath"
1011 "sort"
12+ "strings"
1113 "sync"
1214)
1315
16+ // ErrInvalidPathComponent is returned when a path component contains unsafe characters.
17+ var ErrInvalidPathComponent = errors .New ("invalid path component: contains path separator or traversal sequence" )
18+
19+ // validatePathComponent checks that a string is safe to use as a path component.
20+ // It rejects empty strings, path separators, and traversal sequences.
21+ func validatePathComponent (s string ) error {
22+ if s == "" {
23+ return errors .New ("path component cannot be empty" )
24+ }
25+ if strings .ContainsAny (s , `/\` ) || strings .Contains (s , ".." ) {
26+ return ErrInvalidPathComponent
27+ }
28+ return nil
29+ }
30+
1431// FileBackend implements StorageBackend using JSONL files.
1532// Storage layout:
1633//
@@ -56,6 +73,14 @@ func (f *FileBackend) SaveSession(ctx context.Context, meta *SessionMetadata) er
5673 return ErrStorageClosed
5774 }
5875
76+ // Validate path components to prevent path traversal
77+ if err := validatePathComponent (meta .AgentName ); err != nil {
78+ return fmt .Errorf ("invalid agent name: %w" , err )
79+ }
80+ if err := validatePathComponent (meta .ID ); err != nil {
81+ return fmt .Errorf ("invalid session ID: %w" , err )
82+ }
83+
5984 // Ensure agent directory exists
6085 agentDir := filepath .Join (f .baseDir , meta .AgentName )
6186 if err := os .MkdirAll (agentDir , 0700 ); err != nil {
@@ -66,7 +91,7 @@ func (f *FileBackend) SaveSession(ctx context.Context, meta *SessionMetadata) er
6691 indexPath := filepath .Join (agentDir , "sessions.json" )
6792 index := make (map [string ]* SessionMetadata )
6893
69- data , err := os .ReadFile (indexPath ) // #nosec G304 - path is constructed from trusted base
94+ data , err := os .ReadFile (indexPath ) // #nosec G304 - path components validated to prevent traversal
7095 if err == nil {
7196 if err := json .Unmarshal (data , & index ); err != nil {
7297 return fmt .Errorf ("parse sessions index: %w" , err )
@@ -100,6 +125,11 @@ func (f *FileBackend) LoadSession(ctx context.Context, sessionID string) (*Sessi
100125 return nil , ErrStorageClosed
101126 }
102127
128+ // Validate session ID to prevent path traversal
129+ if err := validatePathComponent (sessionID ); err != nil {
130+ return nil , fmt .Errorf ("invalid session ID: %w" , err )
131+ }
132+
103133 // Search all agent directories for the session
104134 entries , err := os .ReadDir (f .baseDir )
105135 if err != nil {
@@ -115,7 +145,7 @@ func (f *FileBackend) LoadSession(ctx context.Context, sessionID string) (*Sessi
115145 }
116146
117147 indexPath := filepath .Join (f .baseDir , entry .Name (), "sessions.json" )
118- data , err := os .ReadFile (indexPath ) // #nosec G304 - path is constructed from trusted base
148+ data , err := os .ReadFile (indexPath ) // #nosec G304 - path components validated to prevent traversal
119149 if err != nil {
120150 continue
121151 }
@@ -156,7 +186,7 @@ func (f *FileBackend) DeleteSession(ctx context.Context, sessionID string) error
156186
157187 // Remove from index
158188 indexPath := filepath .Join (agentDir , "sessions.json" )
159- data , err := os .ReadFile (indexPath ) // #nosec G304 - path is constructed from trusted base
189+ data , err := os .ReadFile (indexPath ) // #nosec G304 - path components validated to prevent traversal
160190 if err != nil {
161191 return fmt .Errorf ("read sessions index: %w" , err )
162192 }
@@ -189,10 +219,15 @@ func (f *FileBackend) ListSessions(ctx context.Context, agentName string, opts L
189219 return nil , ErrStorageClosed
190220 }
191221
222+ // Validate agent name to prevent path traversal
223+ if err := validatePathComponent (agentName ); err != nil {
224+ return nil , fmt .Errorf ("invalid agent name: %w" , err )
225+ }
226+
192227 agentDir := filepath .Join (f .baseDir , agentName )
193228 indexPath := filepath .Join (agentDir , "sessions.json" )
194229
195- data , err := os .ReadFile (indexPath ) // #nosec G304 - path is constructed from trusted base
230+ data , err := os .ReadFile (indexPath ) // #nosec G304 - path validated above
196231 if err != nil {
197232 if os .IsNotExist (err ) {
198233 return []* SessionMetadata {}, nil
@@ -254,7 +289,7 @@ func (f *FileBackend) AppendEntry(ctx context.Context, sessionID string, entry *
254289 entriesPath := filepath .Join (agentDir , sessionID + ".jsonl" )
255290
256291 // Open file for append
257- file , err := os .OpenFile (entriesPath , os .O_APPEND | os .O_CREATE | os .O_WRONLY , 0600 ) // #nosec G304 - path is constructed from trusted base
292+ file , err := os .OpenFile (entriesPath , os .O_APPEND | os .O_CREATE | os .O_WRONLY , 0600 ) // #nosec G304 - path components validated to prevent traversal
258293 if err != nil {
259294 return fmt .Errorf ("open entries file: %w" , err )
260295 }
@@ -291,7 +326,7 @@ func (f *FileBackend) LoadEntries(ctx context.Context, sessionID string) ([]*Ses
291326 agentDir := filepath .Join (f .baseDir , meta .AgentName )
292327 entriesPath := filepath .Join (agentDir , sessionID + ".jsonl" )
293328
294- file , err := os .Open (entriesPath ) // #nosec G304 - path is constructed from trusted base
329+ file , err := os .Open (entriesPath ) // #nosec G304 - path components validated to prevent traversal
295330 if err != nil {
296331 if os .IsNotExist (err ) {
297332 return []* SessionEntry {}, nil
@@ -326,7 +361,12 @@ func (f *FileBackend) SaveCheckpoint(ctx context.Context, checkpoint *Checkpoint
326361 return ErrStorageClosed
327362 }
328363
329- // Find the session to get the agent name
364+ // Validate checkpoint ID to prevent path traversal
365+ if err := validatePathComponent (checkpoint .ID ); err != nil {
366+ return fmt .Errorf ("invalid checkpoint ID: %w" , err )
367+ }
368+
369+ // Find the session to get the agent name (validates sessionID)
330370 meta , err := f .loadSessionUnlocked (checkpoint .SessionID )
331371 if err != nil {
332372 return err
@@ -360,6 +400,11 @@ func (f *FileBackend) LoadCheckpoint(ctx context.Context, checkpointID string) (
360400 return nil , ErrStorageClosed
361401 }
362402
403+ // Validate checkpoint ID to prevent path traversal
404+ if err := validatePathComponent (checkpointID ); err != nil {
405+ return nil , fmt .Errorf ("invalid checkpoint ID: %w" , err )
406+ }
407+
363408 // Search all agent directories for the checkpoint
364409 entries , err := os .ReadDir (f .baseDir )
365410 if err != nil {
@@ -375,7 +420,7 @@ func (f *FileBackend) LoadCheckpoint(ctx context.Context, checkpointID string) (
375420 }
376421
377422 checkpointPath := filepath .Join (f .baseDir , entry .Name (), "checkpoints" , checkpointID + ".json" )
378- data , err := os .ReadFile (checkpointPath ) // #nosec G304 - path is constructed from trusted base
423+ data , err := os .ReadFile (checkpointPath ) // #nosec G304 - path components validated to prevent traversal
379424 if err != nil {
380425 continue
381426 }
@@ -403,6 +448,11 @@ func (f *FileBackend) Close() error {
403448// loadSessionUnlocked is an internal helper that loads session without acquiring locks.
404449// Caller must hold appropriate lock.
405450func (f * FileBackend ) loadSessionUnlocked (sessionID string ) (* SessionMetadata , error ) {
451+ // Validate session ID to prevent path traversal
452+ if err := validatePathComponent (sessionID ); err != nil {
453+ return nil , fmt .Errorf ("invalid session ID: %w" , err )
454+ }
455+
406456 // Search all agent directories for the session
407457 entries , err := os .ReadDir (f .baseDir )
408458 if err != nil {
@@ -418,7 +468,7 @@ func (f *FileBackend) loadSessionUnlocked(sessionID string) (*SessionMetadata, e
418468 }
419469
420470 indexPath := filepath .Join (f .baseDir , entry .Name (), "sessions.json" )
421- data , err := os .ReadFile (indexPath ) // #nosec G304 - path is constructed from trusted base
471+ data , err := os .ReadFile (indexPath ) // #nosec G304 - path components validated to prevent traversal
422472 if err != nil {
423473 continue
424474 }
0 commit comments