diff --git a/internal/services/lambda/store.go b/internal/services/lambda/store.go index 1debbde..ed4997f 100644 --- a/internal/services/lambda/store.go +++ b/internal/services/lambda/store.go @@ -144,14 +144,43 @@ func (s *LambdaStore) Close() error { return s.store.Close() } +// validPathComponent returns true if s is a single path component with no +// separators or traversal sequences. +func validPathComponent(s string) bool { + return !strings.ContainsAny(s, "/\\") && !strings.Contains(s, "..") +} + // codePath returns the filesystem path for a function's code zip. // It validates the result stays under codeDir to prevent path traversal. func (s *LambdaStore) codePath(accountID, functionName string) (string, error) { + // accountID and functionName are expected to be single path components. + if accountID == "" || functionName == "" { + return "", fmt.Errorf("invalid empty path component") + } + if !validPathComponent(accountID) { + return "", fmt.Errorf("invalid account id path component") + } + if !validPathComponent(functionName) { + return "", fmt.Errorf("invalid function name path component") + } + joined := filepath.Join(s.codeDir, accountID, functionName, "code.zip") cleaned := filepath.Clean(joined) - absBase, _ := filepath.Abs(s.codeDir) - absCleaned, _ := filepath.Abs(cleaned) - if !strings.HasPrefix(absCleaned, absBase+string(filepath.Separator)) { + + absBase, err := filepath.Abs(s.codeDir) + if err != nil { + return "", fmt.Errorf("resolve base code directory: %w", err) + } + absCleaned, err := filepath.Abs(cleaned) + if err != nil { + return "", fmt.Errorf("resolve code path: %w", err) + } + + rel, err := filepath.Rel(absBase, absCleaned) + if err != nil { + return "", fmt.Errorf("resolve relative code path: %w", err) + } + if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { return "", fmt.Errorf("path traversal detected: %s", cleaned) } return cleaned, nil