...

Source file src/github.com/cyphar/filepath-securejoin/join.go

Documentation: github.com/cyphar/filepath-securejoin

     1  // Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
     2  // Copyright (C) 2017 SUSE LLC. All rights reserved.
     3  // Use of this source code is governed by a BSD-style
     4  // license that can be found in the LICENSE file.
     5  
     6  // Package securejoin is an implementation of the hopefully-soon-to-be-included
     7  // SecureJoin helper that is meant to be part of the "path/filepath" package.
     8  // The purpose of this project is to provide a PoC implementation to make the
     9  // SecureJoin proposal (https://github.com/golang/go/issues/20126) more
    10  // tangible.
    11  package securejoin
    12  
    13  import (
    14  	"bytes"
    15  	"errors"
    16  	"os"
    17  	"path/filepath"
    18  	"strings"
    19  	"syscall"
    20  )
    21  
    22  // IsNotExist tells you if err is an error that implies that either the path
    23  // accessed does not exist (or path components don't exist). This is
    24  // effectively a more broad version of os.IsNotExist.
    25  func IsNotExist(err error) bool {
    26  	// Check that it's not actually an ENOTDIR, which in some cases is a more
    27  	// convoluted case of ENOENT (usually involving weird paths).
    28  	return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT)
    29  }
    30  
    31  // SecureJoinVFS joins the two given path components (similar to Join) except
    32  // that the returned path is guaranteed to be scoped inside the provided root
    33  // path (when evaluated). Any symbolic links in the path are evaluated with the
    34  // given root treated as the root of the filesystem, similar to a chroot. The
    35  // filesystem state is evaluated through the given VFS interface (if nil, the
    36  // standard os.* family of functions are used).
    37  //
    38  // Note that the guarantees provided by this function only apply if the path
    39  // components in the returned string are not modified (in other words are not
    40  // replaced with symlinks on the filesystem) after this function has returned.
    41  // Such a symlink race is necessarily out-of-scope of SecureJoin.
    42  //
    43  // Volume names in unsafePath are always discarded, regardless if they are
    44  // provided via direct input or when evaluating symlinks. Therefore:
    45  //
    46  // "C:\Temp" + "D:\path\to\file.txt" results in "C:\Temp\path\to\file.txt"
    47  func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) {
    48  	// Use the os.* VFS implementation if none was specified.
    49  	if vfs == nil {
    50  		vfs = osVFS{}
    51  	}
    52  
    53  	unsafePath = filepath.FromSlash(unsafePath)
    54  	var path bytes.Buffer
    55  	n := 0
    56  	for unsafePath != "" {
    57  		if n > 255 {
    58  			return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP}
    59  		}
    60  
    61  		if v := filepath.VolumeName(unsafePath); v != "" {
    62  			unsafePath = unsafePath[len(v):]
    63  		}
    64  
    65  		// Next path component, p.
    66  		i := strings.IndexRune(unsafePath, filepath.Separator)
    67  		var p string
    68  		if i == -1 {
    69  			p, unsafePath = unsafePath, ""
    70  		} else {
    71  			p, unsafePath = unsafePath[:i], unsafePath[i+1:]
    72  		}
    73  
    74  		// Create a cleaned path, using the lexical semantics of /../a, to
    75  		// create a "scoped" path component which can safely be joined to fullP
    76  		// for evaluation. At this point, path.String() doesn't contain any
    77  		// symlink components.
    78  		cleanP := filepath.Clean(string(filepath.Separator) + path.String() + p)
    79  		if cleanP == string(filepath.Separator) {
    80  			path.Reset()
    81  			continue
    82  		}
    83  		fullP := filepath.Clean(root + cleanP)
    84  
    85  		// Figure out whether the path is a symlink.
    86  		fi, err := vfs.Lstat(fullP)
    87  		if err != nil && !IsNotExist(err) {
    88  			return "", err
    89  		}
    90  		// Treat non-existent path components the same as non-symlinks (we
    91  		// can't do any better here).
    92  		if IsNotExist(err) || fi.Mode()&os.ModeSymlink == 0 {
    93  			path.WriteString(p)
    94  			path.WriteRune(filepath.Separator)
    95  			continue
    96  		}
    97  
    98  		// Only increment when we actually dereference a link.
    99  		n++
   100  
   101  		// It's a symlink, expand it by prepending it to the yet-unparsed path.
   102  		dest, err := vfs.Readlink(fullP)
   103  		if err != nil {
   104  			return "", err
   105  		}
   106  		// Absolute symlinks reset any work we've already done.
   107  		if filepath.IsAbs(dest) {
   108  			path.Reset()
   109  		}
   110  		unsafePath = dest + string(filepath.Separator) + unsafePath
   111  	}
   112  
   113  	// We have to clean path.String() here because it may contain '..'
   114  	// components that are entirely lexical, but would be misleading otherwise.
   115  	// And finally do a final clean to ensure that root is also lexically
   116  	// clean.
   117  	fullP := filepath.Clean(string(filepath.Separator) + path.String())
   118  	return filepath.Clean(root + fullP), nil
   119  }
   120  
   121  // SecureJoin is a wrapper around SecureJoinVFS that just uses the os.* library
   122  // of functions as the VFS. If in doubt, use this function over SecureJoinVFS.
   123  func SecureJoin(root, unsafePath string) (string, error) {
   124  	return SecureJoinVFS(root, unsafePath, nil)
   125  }
   126  

View as plain text