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) { func openPublishRunFiles(runRoot string, files []publishUploadFile) ([]*os.File, error) {
sources := make([]*os.File, 0, len(files)) sources := make([]*os.File, 0, len(files))
for _, candidate := range files { for _, candidate := range files {
file, err := openPublishOutputSource(filepath.Join(runRoot, filepath.FromSlash(candidate.SourceRelativePath))) file, err := openPublishSourceUnderRoot(runRoot, candidate.SourceRelativePath)
if err != nil { if err != nil {
closePublishSources(sources) closePublishSources(sources)
return nil, err return nil, err
@@ -993,6 +993,56 @@ func openPublishRunFiles(runRoot string, files []publishUploadFile) ([]*os.File,
return sources, nil 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 { func verifyPublishSourceChecksum(file *os.File, relativePath, expected string) error {
if strings.TrimSpace(expected) == "" { if strings.TrimSpace(expected) == "" {
return nil return nil

View File

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