github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/file/find.go (about)

     1  package file
     2  
     3  import (
     4  	"io"
     5  	"io/fs"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/pkg/errors"
    11  	"github.com/projectdiscovery/gologger"
    12  	fileutil "github.com/projectdiscovery/utils/file"
    13  	folderutil "github.com/projectdiscovery/utils/folder"
    14  )
    15  
    16  // getInputPaths parses the specified input paths and returns a compiled
    17  // list of finished absolute paths to the files evaluating any allowlist, denylist,
    18  // glob, file or folders, etc.
    19  func (request *Request) getInputPaths(target string, callback func(string)) error {
    20  	processed := make(map[string]struct{})
    21  
    22  	// Template input includes a wildcard
    23  	if strings.Contains(target, "*") && !request.NoRecursive {
    24  		if err := request.findGlobPathMatches(target, processed, callback); err != nil {
    25  			return errors.Wrap(err, "could not find glob matches")
    26  		}
    27  		return nil
    28  	}
    29  
    30  	// Template input is either a file or a directory
    31  	file, err := request.findFileMatches(target, processed, callback)
    32  	if err != nil {
    33  		return errors.Wrap(err, "could not find file")
    34  	}
    35  	if file {
    36  		return nil
    37  	}
    38  	if request.NoRecursive {
    39  		return nil // we don't process dirs in no-recursive mode
    40  	}
    41  	// Recursively walk down the Templates directory and run all
    42  	// the template file checks
    43  	if err := request.findDirectoryMatches(target, processed, callback); err != nil {
    44  		return errors.Wrap(err, "could not find directory matches")
    45  	}
    46  	return nil
    47  }
    48  
    49  // findGlobPathMatches returns the matched files from a glob path
    50  func (request *Request) findGlobPathMatches(absPath string, processed map[string]struct{}, callback func(string)) error {
    51  	matches, err := filepath.Glob(absPath)
    52  	if err != nil {
    53  		return errors.Errorf("wildcard found, but unable to glob: %s\n", err)
    54  	}
    55  	for _, match := range matches {
    56  		if !request.validatePath(absPath, match, false) {
    57  			continue
    58  		}
    59  		if _, ok := processed[match]; !ok {
    60  			processed[match] = struct{}{}
    61  			callback(match)
    62  		}
    63  	}
    64  	return nil
    65  }
    66  
    67  // findFileMatches finds if a path is an absolute file. If the path
    68  // is a file, it returns true otherwise false with no errors.
    69  func (request *Request) findFileMatches(absPath string, processed map[string]struct{}, callback func(string)) (bool, error) {
    70  	info, err := os.Stat(absPath)
    71  	if err != nil {
    72  		return false, err
    73  	}
    74  	if !info.Mode().IsRegular() {
    75  		return false, nil
    76  	}
    77  	if _, ok := processed[absPath]; !ok {
    78  		if !request.validatePath(absPath, absPath, false) {
    79  			return false, nil
    80  		}
    81  		processed[absPath] = struct{}{}
    82  		callback(absPath)
    83  	}
    84  	return true, nil
    85  }
    86  
    87  // findDirectoryMatches finds matches for templates from a directory
    88  func (request *Request) findDirectoryMatches(absPath string, processed map[string]struct{}, callback func(string)) error {
    89  	err := filepath.WalkDir(
    90  		absPath,
    91  		func(path string, d fs.DirEntry, err error) error {
    92  			// continue on errors
    93  			if err != nil {
    94  				return nil
    95  			}
    96  			if d.IsDir() {
    97  				return nil
    98  			}
    99  			if !request.validatePath(absPath, path, false) {
   100  				return nil
   101  			}
   102  			if _, ok := processed[path]; !ok {
   103  				callback(path)
   104  				processed[path] = struct{}{}
   105  			}
   106  			return nil
   107  		},
   108  	)
   109  	return err
   110  }
   111  
   112  // validatePath validates a file path for blacklist and whitelist options
   113  func (request *Request) validatePath(absPath, item string, inArchive bool) bool {
   114  	extension := filepath.Ext(item)
   115  	// extension check
   116  	if len(request.extensions) > 0 {
   117  		if _, ok := request.extensions[extension]; ok {
   118  			return true
   119  		} else if !request.allExtensions {
   120  			return false
   121  		}
   122  	}
   123  
   124  	var (
   125  		fileExists bool
   126  		dataChunk  []byte
   127  	)
   128  	if !inArchive && request.MimeType {
   129  		// mime type check
   130  		// read first bytes to infer runtime type
   131  		fileExists = fileutil.FileExists(item)
   132  		if fileExists {
   133  			dataChunk, _ = readChunk(item)
   134  			if len(request.mimeTypesChecks) > 0 && matchAnyMimeTypes(dataChunk, request.mimeTypesChecks) {
   135  				return true
   136  			}
   137  		}
   138  	}
   139  
   140  	if matchingRule, ok := request.isInDenyList(absPath, item); ok {
   141  		gologger.Verbose().Msgf("Ignoring path %s due to denylist item %s\n", item, matchingRule)
   142  		return false
   143  	}
   144  
   145  	// denied mime type checks
   146  	if !inArchive && request.MimeType && fileExists {
   147  		if len(request.denyMimeTypesChecks) > 0 && matchAnyMimeTypes(dataChunk, request.denyMimeTypesChecks) {
   148  			return false
   149  		}
   150  	}
   151  
   152  	return true
   153  }
   154  
   155  func (request *Request) isInDenyList(absPath, item string) (string, bool) {
   156  	extension := filepath.Ext(item)
   157  	// check for possible deny rules
   158  	// - extension is in deny list
   159  	if _, ok := request.denyList[extension]; ok {
   160  		return extension, true
   161  	}
   162  
   163  	// - full path is in deny list
   164  	if _, ok := request.denyList[item]; ok {
   165  		return item, true
   166  	}
   167  
   168  	// file is in a forbidden subdirectory
   169  	filename := filepath.Base(item)
   170  	fullPathWithoutFilename := strings.TrimSuffix(item, filename)
   171  	relativePathWithFilename := strings.TrimPrefix(item, absPath)
   172  	relativePath := strings.TrimSuffix(relativePathWithFilename, filename)
   173  
   174  	// - filename is in deny list
   175  	if _, ok := request.denyList[filename]; ok {
   176  		return filename, true
   177  	}
   178  
   179  	// - relative path is in deny list
   180  	if _, ok := request.denyList[relativePath]; ok {
   181  		return relativePath, true
   182  	}
   183  
   184  	// relative path + filename are in the forbidden list
   185  	if _, ok := request.denyList[relativePathWithFilename]; ok {
   186  		return relativePathWithFilename, true
   187  	}
   188  
   189  	// root path + relative path are in the forbidden list
   190  	if _, ok := request.denyList[fullPathWithoutFilename]; ok {
   191  		return fullPathWithoutFilename, true
   192  	}
   193  
   194  	// check any progressive combined part of the relative and absolute path with filename for matches within rules prefixes
   195  	if pathTreeItem, ok := request.isAnyChunkInDenyList(relativePath, false); ok {
   196  		return pathTreeItem, true
   197  	}
   198  	if pathTreeItem, ok := request.isAnyChunkInDenyList(item, true); ok {
   199  		return pathTreeItem, true
   200  	}
   201  
   202  	return "", false
   203  }
   204  
   205  func readChunk(fileName string) ([]byte, error) {
   206  	r, err := os.Open(fileName)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  
   211  	defer r.Close()
   212  
   213  	var buff [1024]byte
   214  	if _, err = io.ReadFull(r, buff[:]); err != nil {
   215  		return nil, err
   216  	}
   217  	return buff[:], nil
   218  }
   219  
   220  func (request *Request) isAnyChunkInDenyList(path string, splitWithUtils bool) (string, bool) {
   221  	var paths []string
   222  
   223  	if splitWithUtils {
   224  		pathInfo, _ := folderutil.NewPathInfo(path)
   225  		paths, _ = pathInfo.Paths()
   226  	} else {
   227  		pathTree := strings.Split(path, string(os.PathSeparator))
   228  		for i := range pathTree {
   229  			paths = append(paths, filepath.Join(pathTree[:i]...))
   230  		}
   231  	}
   232  	for _, pathTreeItem := range paths {
   233  		if _, ok := request.denyList[pathTreeItem]; ok {
   234  			return pathTreeItem, true
   235  		}
   236  	}
   237  
   238  	return "", false
   239  }