Skip to content

Commit 6d25864

Browse files
fix: prevent path traversal in saveParameter and saveArtifact
Normalize absolute source paths by stripping the leading slash before building the destination path (e.g. /tmp/artifact → tmp/artifact). Validate the resulting relative path with filepath.IsLocal to block any .. traversal, then create the destination file through os.OpenRoot so the OS enforces the directory boundary and symlink escapes are also prevented. Signed-off-by: Jayant Kamble <jayantkamble10000@gmail.com>
1 parent 80dc102 commit 6d25864

2 files changed

Lines changed: 82 additions & 10 deletions

File tree

cmd/argoexec/commands/emissary.go

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -401,20 +401,33 @@ func saveArtifact(ctx context.Context, srcPath string) error {
401401
logger.WithField("srcPath", srcPath).Info(ctx, "no need to save artifact - on overlapping volume")
402402
return nil
403403
}
404+
// Normalize absolute paths (e.g. /tmp/artifact → tmp/artifact) so that
405+
// filepath.IsLocal can validate the result, and os.Root.Create can safely
406+
// write it within the outputs directory.
407+
relSrc := strings.TrimPrefix(strings.TrimSuffix(srcPath, "/"), "/")
408+
dstRel := relSrc + ".tgz"
409+
if !filepath.IsLocal(dstRel) {
410+
return fmt.Errorf("path traversal in output artifact path %q", srcPath)
411+
}
412+
artifactsDir := filepath.Join(varRunArgo, "outputs/artifacts")
413+
dstPath := filepath.Join(artifactsDir, dstRel)
404414
if _, err := os.Stat(srcPath); os.IsNotExist(err) { // might be optional, so we ignore
405415
logger.WithField("srcPath", srcPath).WithError(err).Warn(ctx, "cannot save artifact")
406416
return nil
407417
}
408-
dstPath := filepath.Join(varRunArgo, "/outputs/artifacts/", strings.TrimSuffix(srcPath, "/")+".tgz")
409418
logger.WithFields(logging.Fields{
410419
"src": srcPath,
411420
"dst": dstPath,
412421
}).Info(ctx, "saving artifact")
413-
z := filepath.Dir(dstPath)
414-
if err := os.MkdirAll(z, 0o755); err != nil { // chmod rwxr-xr-x
415-
return fmt.Errorf("failed to create directory %s: %w", z, err)
422+
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil { // chmod rwxr-xr-x
423+
return fmt.Errorf("failed to create directory %s: %w", filepath.Dir(dstPath), err)
424+
}
425+
root, err := os.OpenRoot(artifactsDir)
426+
if err != nil {
427+
return fmt.Errorf("failed to open artifacts output root: %w", err)
416428
}
417-
dst, err := os.Create(dstPath)
429+
defer root.Close()
430+
dst, err := root.Create(dstRel)
418431
if err != nil {
419432
return fmt.Errorf("failed to create destination %s: %w", dstPath, err)
420433
}
@@ -435,6 +448,15 @@ func saveParameter(ctx context.Context, srcPath string) error {
435448
logger.WithField("src", srcPath).Info(ctx, "no need to save parameter - on overlapping volume")
436449
return nil
437450
}
451+
// Normalize absolute paths (e.g. /tmp/parameter → tmp/parameter) so that
452+
// filepath.IsLocal can validate the result, and os.Root.Create can safely
453+
// write it within the outputs directory.
454+
relPath := strings.TrimPrefix(srcPath, "/")
455+
if !filepath.IsLocal(relPath) {
456+
return fmt.Errorf("path traversal in output parameter path %q", srcPath)
457+
}
458+
parametersDir := filepath.Join(varRunArgo, "outputs/parameters")
459+
dstPath := filepath.Join(parametersDir, relPath)
438460
src, err := os.Open(filepath.Clean(srcPath))
439461
if os.IsNotExist(err) { // might be optional, so we ignore
440462
logger.WithField("src", srcPath).WithError(err).Error(ctx, "cannot save parameter, does not exist")
@@ -444,16 +466,19 @@ func saveParameter(ctx context.Context, srcPath string) error {
444466
return fmt.Errorf("failed to open %s: %w", srcPath, err)
445467
}
446468
defer func() { _ = src.Close() }()
447-
dstPath := varRunArgo + "/outputs/parameters/" + srcPath
448469
logger.WithFields(logging.Fields{
449470
"src": srcPath,
450471
"dst": dstPath,
451472
}).Info(ctx, "saving parameter")
452-
z := filepath.Dir(dstPath)
453-
if mkdirErr := os.MkdirAll(z, 0o755); mkdirErr != nil { // chmod rwxr-xr-x
454-
return fmt.Errorf("failed to create directory %s: %w", z, mkdirErr)
473+
if mkdirErr := os.MkdirAll(filepath.Dir(dstPath), 0o755); mkdirErr != nil { // chmod rwxr-xr-x
474+
return fmt.Errorf("failed to create directory %s: %w", filepath.Dir(dstPath), mkdirErr)
475+
}
476+
root, err := os.OpenRoot(parametersDir)
477+
if err != nil {
478+
return fmt.Errorf("failed to open parameters output root: %w", err)
455479
}
456-
dst, err := os.Create(dstPath)
480+
defer root.Close()
481+
dst, err := root.Create(relPath)
457482
if err != nil {
458483
return fmt.Errorf("failed to create %s: %w", srcPath, err)
459484
}

cmd/argoexec/commands/emissary_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package commands
55
import (
66
"fmt"
77
"os"
8+
"path/filepath"
89
"strconv"
910
"sync"
1011
"syscall"
@@ -205,6 +206,52 @@ func TestEmissary(t *testing.T) {
205206
})
206207
}
207208

209+
func TestSaveParameterPathTraversal(t *testing.T) {
210+
tmp := t.TempDir()
211+
varRunArgo = tmp
212+
ctx := logging.TestContext(t.Context())
213+
214+
srcFile := "result.txt"
215+
require.NoError(t, os.WriteFile(filepath.Join(tmp, srcFile), []byte("hello"), 0o644))
216+
217+
t.Run("LegitimateRelativePath", func(t *testing.T) {
218+
err := saveParameter(ctx, srcFile)
219+
require.NoError(t, err)
220+
})
221+
t.Run("TraversalToArgoexec", func(t *testing.T) {
222+
err := saveParameter(ctx, "../../argoexec")
223+
require.Error(t, err)
224+
assert.Contains(t, err.Error(), "path traversal")
225+
})
226+
t.Run("TraversalToSidecarExitCode", func(t *testing.T) {
227+
err := saveParameter(ctx, "../../ctr/sidecar/exitcode")
228+
require.Error(t, err)
229+
assert.Contains(t, err.Error(), "path traversal")
230+
})
231+
t.Run("TraversalToTemplate", func(t *testing.T) {
232+
err := saveParameter(ctx, "../template")
233+
require.Error(t, err)
234+
assert.Contains(t, err.Error(), "path traversal")
235+
})
236+
}
237+
238+
func TestSaveArtifactPathTraversal(t *testing.T) {
239+
tmp := t.TempDir()
240+
varRunArgo = tmp
241+
ctx := logging.TestContext(t.Context())
242+
243+
t.Run("TraversalToArgoexec", func(t *testing.T) {
244+
err := saveArtifact(ctx, "../../argoexec")
245+
require.Error(t, err)
246+
assert.Contains(t, err.Error(), "path traversal")
247+
})
248+
t.Run("TraversalToSidecarExitCode", func(t *testing.T) {
249+
err := saveArtifact(ctx, "../../ctr/sidecar/exitcode")
250+
require.Error(t, err)
251+
assert.Contains(t, err.Error(), "path traversal")
252+
})
253+
}
254+
208255
func run(script string) error {
209256
cmd := NewEmissaryCommand()
210257
_, _, err := cmdutil.ContextWithLogger(cmd, string(logging.Info), string(logging.Text))

0 commit comments

Comments
 (0)