Confine published diagnostic files

This commit is contained in:
2026-09-04 00:07:50 +00:00
parent 1da34ab5cd
commit df275874e3
2 changed files with 56 additions and 4 deletions

View File

@@ -978,7 +978,7 @@ func collectPublishPreviousFiles(ctx context.Context, previousDir string) ([]pub
func openPublishRunFiles(runRoot string, files []publishUploadFile) ([]*os.File, error) {
sources := make([]*os.File, 0, len(files))
for _, candidate := range files {
file, err := openPublishOutputSource(filepath.Join(runRoot, filepath.FromSlash(candidate.SourceRelativePath)))
file, err := openPublishSourceUnderRoot(runRoot, candidate.SourceRelativePath)
if err != nil {
closePublishSources(sources)
return nil, err
@@ -993,6 +993,56 @@ func openPublishRunFiles(runRoot string, files []publishUploadFile) ([]*os.File,
return sources, nil
}
func openPublishSourceUnderRoot(rootPath, relativePath string) (*os.File, error) {
relativePath, err := pathsafe.NormalizeRelativeDestination(relativePath)
if err != nil {
return nil, fmt.Errorf("validate publish source path %q: %w", relativePath, err)
}
root, err := os.OpenRoot(rootPath)
if err != nil {
return nil, fmt.Errorf("open publish source root %q: %w", rootPath, err)
}
openedRoots := []*os.Root{root}
defer func() {
for index := len(openedRoots) - 1; index >= 0; index-- {
_ = openedRoots[index].Close()
}
}()
parts := strings.Split(relativePath, "/")
current := root
for _, part := range parts[:len(parts)-1] {
info, err := current.Lstat(part)
if err != nil {
return nil, fmt.Errorf("inspect publish source ancestor %q: %w", part, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return nil, fmt.Errorf("publish source ancestor %q is not a directory without symlinks", part)
}
next, err := current.OpenRoot(part)
if err != nil {
return nil, fmt.Errorf("open publish source ancestor %q: %w", part, err)
}
openedRoots = append(openedRoots, next)
current = next
}
leaf := parts[len(parts)-1]
info, err := current.Lstat(leaf)
if err != nil {
return nil, fmt.Errorf("inspect publish source %q: %w", relativePath, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return nil, fmt.Errorf("publish source %q is not a regular file without symlinks", relativePath)
}
file, err := current.Open(leaf)
if err != nil {
return nil, fmt.Errorf("open publish source %q: %w", relativePath, err)
}
return file, nil
}
func verifyPublishSourceChecksum(file *os.File, relativePath, expected string) error {
if strings.TrimSpace(expected) == "" {
return nil

View File

@@ -263,7 +263,7 @@ func TestPublishRejectsSymlinkedRunRootBeforeUploading(t *testing.T) {
assertNoPublishUploads(t, env.ObjectStore.(*storage.FakeBackend), "")
}
func TestPublishUsesOrdinarySourceAncestorPathSemantics(t *testing.T) {
func TestPublishRejectsDeclaredSourceWithSymlinkedAncestor(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink creation requires privileges on Windows")
}
@@ -278,9 +278,11 @@ func TestPublishUsesOrdinarySourceAncestorPathSemantics(t *testing.T) {
t.Fatal(err)
}
if _, err := (publishStage{}).Run(context.Background(), env, m); err != nil {
t.Fatalf("Run() error = %v", err)
_, err := (publishStage{}).Run(context.Background(), env, m)
if err == nil || !strings.Contains(err.Error(), "ancestor") {
t.Fatalf("Run() error = %v, want rejected symlinked ancestor", err)
}
assertNoPublishUploads(t, env.ObjectStore.(*storage.FakeBackend), "outside secret")
}
func TestPublishRejectsDeclaredSourceReplacement(t *testing.T) {