github.com/yogeshkumararora/slsa-github-generator@v1.10.1-0.20240520161934-11278bd5afb4/internal/utils/path.go (about)

     1  // Copyright 2022 SLSA Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package utils
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  )
    25  
    26  var (
    27  	// ErrInternal indicates an internal error.
    28  	ErrInternal = errors.New("internal error")
    29  
    30  	// ErrInvalidPath indicates an invalid path.
    31  	ErrInvalidPath = errors.New("invalid path")
    32  )
    33  
    34  // PathIsUnderCurrentDirectory checks whether the `path`
    35  // is under the current working directory. Examples:
    36  // ./file, ./some/path, ../<cwd>.file would return `nil`.
    37  // `../etc/password` would return an error.
    38  func PathIsUnderCurrentDirectory(path string) error {
    39  	wd, err := os.Getwd()
    40  	if err != nil {
    41  		return fmt.Errorf("%w: os.Getwd(): %w", ErrInternal, err)
    42  	}
    43  	p, err := filepath.Abs(path)
    44  	if err != nil {
    45  		return fmt.Errorf("%w: filepath.Abs(): %w", ErrInternal, err)
    46  	}
    47  	return checkPathUnderDir(p, wd)
    48  }
    49  
    50  // PathIsUnderDirectory checks to see if path is under the absolute
    51  // directory specified.
    52  func PathIsUnderDirectory(path, absoluteDir string) error {
    53  	p, err := filepath.Abs(filepath.Join(absoluteDir, path))
    54  	if err != nil {
    55  		return fmt.Errorf("%w: filepath.Abs(): %w", ErrInternal, err)
    56  	}
    57  
    58  	return checkPathUnderDir(p, absoluteDir)
    59  }
    60  
    61  func checkPathUnderDir(p, dir string) error {
    62  	if !strings.HasPrefix(p, dir+"/") &&
    63  		dir != p {
    64  		return fmt.Errorf("%w: %q", ErrInvalidPath, p)
    65  	}
    66  	return nil
    67  }
    68  
    69  // VerifyAttestationPath verifies that the path of an attestation
    70  // is valid. It checks that the path is under the current working directory
    71  // and that the extension of the file is `intoto.jsonl`.
    72  func VerifyAttestationPath(path string) error {
    73  	if !strings.HasSuffix(path, "intoto.jsonl") {
    74  		return fmt.Errorf("%w: suffix of %q must be .intoto.jsonl", ErrInvalidPath, path)
    75  	}
    76  	return PathIsUnderCurrentDirectory(path)
    77  }
    78  
    79  // CreateNewFileUnderCurrentDirectory create a new file under the current directory
    80  // and fails if the file already exists. The file is always created with the pemisisons
    81  // `0o600`.
    82  func CreateNewFileUnderCurrentDirectory(path string, flag int) (io.Writer, error) {
    83  	if path == "-" {
    84  		return os.Stdout, nil
    85  	}
    86  
    87  	if err := PathIsUnderCurrentDirectory(path); err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	// Ensure we never overwrite an existing file.
    92  	fp, err := os.OpenFile(filepath.Clean(path), flag|os.O_CREATE|os.O_EXCL, 0o600)
    93  	if err != nil {
    94  		if errors.Is(err, os.ErrPermission) || errors.Is(err, os.ErrExist) || errors.Is(err, os.ErrNotExist) {
    95  			return nil, fmt.Errorf("%w: os.OpenFile(): %w", ErrInvalidPath, err)
    96  		}
    97  		return nil, fmt.Errorf("%w: os.OpenFile(): %w", ErrInternal, err)
    98  	}
    99  
   100  	return fp, nil
   101  }
   102  
   103  // CreateNewFileUnderDirectory create a new file under the current directory
   104  // and fails if the file already exists. The file is always created with the pemisisons
   105  // `0o600`. Ensures that the path does not exit out of the given directory.
   106  func CreateNewFileUnderDirectory(path, dir string, flag int) (io.Writer, error) {
   107  	if path == "-" {
   108  		return os.Stdout, nil
   109  	}
   110  
   111  	if err := PathIsUnderDirectory(path, dir); err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	// Create the directory if it does not exist
   116  	fullPath := filepath.Join(dir, path)
   117  	err := os.MkdirAll(filepath.Dir(fullPath), 0o755)
   118  	if err != nil {
   119  		return nil, fmt.Errorf("%w: os.MkdirAll(): %w", ErrInternal, err)
   120  	}
   121  
   122  	// Ensure we never overwrite an existing file.
   123  	fp, err := os.OpenFile(filepath.Clean(fullPath), flag|os.O_CREATE|os.O_EXCL, 0o600)
   124  	if err != nil {
   125  		if errors.Is(err, os.ErrPermission) || errors.Is(err, os.ErrExist) || errors.Is(err, os.ErrNotExist) {
   126  			return nil, fmt.Errorf("%w: os.OpenFile(): %w", ErrInvalidPath, err)
   127  		}
   128  		return nil, fmt.Errorf("%w: os.OpenFile(): %w", ErrInternal, err)
   129  	}
   130  
   131  	return fp, nil
   132  }
   133  
   134  // SafeReadFile checks for directory traversal before reading the given file.
   135  func SafeReadFile(path string) ([]byte, error) {
   136  	if err := PathIsUnderCurrentDirectory(path); err != nil {
   137  		return nil, fmt.Errorf("%w: PathIsUnderCurrentDirectory: %w", ErrInternal, err)
   138  	}
   139  	return os.ReadFile(path)
   140  }