diff --git a/interp/allowed_paths.go b/interp/allowed_paths.go index 522395ff..50dce290 100644 --- a/interp/allowed_paths.go +++ b/interp/allowed_paths.go @@ -86,8 +86,13 @@ func toAbs(path, cwd string) string { } // open implements the restricted file-open policy. The file is opened through -// os.Root for atomic path validation. +// os.Root for atomic path validation. Only read-only access is permitted; +// any write flags are rejected as a defense-in-depth measure. func (s *pathSandbox) open(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { + if flag != os.O_RDONLY { + return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrPermission} + } + absPath := toAbs(path, HandlerCtx(ctx).Dir) root, relPath, ok := s.resolve(absPath) diff --git a/interp/allowed_paths_internal_test.go b/interp/allowed_paths_internal_test.go index ef2a7fd9..c268f107 100644 --- a/interp/allowed_paths_internal_test.go +++ b/interp/allowed_paths_internal_test.go @@ -167,3 +167,33 @@ func TestAllowedPathsExecDefaultBlocksAll(t *testing.T) { assert.Equal(t, 127, exitCode) assert.Contains(t, stderr, "command not found") } + +func TestPathSandboxOpenRejectsWriteFlags(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("data"), 0644)) + + sb, err := newPathSandbox([]string{dir}) + require.NoError(t, err) + defer sb.Close() + + ctx := context.WithValue(context.Background(), handlerCtxKey{}, HandlerContext{Dir: dir}) + + writeFlags := []int{ + os.O_WRONLY, + os.O_RDWR, + os.O_APPEND, + os.O_CREATE, + os.O_TRUNC, + os.O_WRONLY | os.O_CREATE | os.O_TRUNC, + } + for _, flag := range writeFlags { + f, err := sb.open(ctx, "test.txt", flag, 0644) + assert.Nil(t, f, "open with flag %d should return nil", flag) + assert.ErrorIs(t, err, os.ErrPermission, "open with flag %d should be denied", flag) + } + + // Read-only should still work. + f, err := sb.open(ctx, "test.txt", os.O_RDONLY, 0) + require.NoError(t, err) + f.Close() +}