go.lang.security.filepath-clean-misuse.filepath-clean-misuse

profile photo of semgrepsemgrep
Author
unknown
Download Count*

Clean is not intended to sanitize against path traversal attacks. This function is for finding the shortest path name equivalent to the given input. Using Clean to sanitize file reads may expose this application to path traversal attacks, where an attacker could access arbitrary files on the server. To fix this easily, write this: filepath.FromSlash(path.Clean("/"+strings.Trim(req.URL.Path, "/"))) However, a better solution is using the SecureJoin function in the package filepath-securejoin. See https://pkg.go.dev/github.com/cyphar/filepath-securejoin#section-readme.

Run Locally

Run in CI

Defintion

rules:
  - id: filepath-clean-misuse
    message: '`Clean` is not intended to sanitize against path traversal attacks.
      This function is for finding the shortest path name equivalent to the
      given input. Using `Clean` to sanitize file reads may expose this
      application to path traversal attacks, where an attacker could access
      arbitrary files on the server. To fix this easily, write this:
      `filepath.FromSlash(path.Clean("/"+strings.Trim(req.URL.Path, "/")))`
      However, a better solution is using the `SecureJoin` function in the
      package `filepath-securejoin`. See
      https://pkg.go.dev/github.com/cyphar/filepath-securejoin#section-readme.'
    severity: ERROR
    languages:
      - go
    mode: taint
    pattern-sources:
      - patterns:
          - pattern-either:
              - pattern: |
                  ($REQUEST : *http.Request).$ANYTHING
              - pattern: |
                  ($REQUEST : http.Request).$ANYTHING
          - metavariable-regex:
              metavariable: $ANYTHING
              regex: ^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$
    pattern-sinks:
      - patterns:
          - pattern-either:
              - pattern: filepath.Clean($...INNER)
              - pattern: path.Clean($...INNER)
    pattern-sanitizers:
      - pattern-either:
          - pattern: |
              "/" + ...
    fix: filepath.FromSlash(filepath.Clean("/"+strings.Trim($...INNER, "/")))
    options:
      interfile: true
    metadata:
      references:
        - https://pkg.go.dev/path#Clean
        - http://technosophos.com/2016/03/31/go-quickly-cleaning-filepaths.html
        - https://labs.detectify.com/2021/12/15/zero-day-path-traversal-grafana/
        - https://dzx.cz/2021/04/02/go_path_traversal/
        - https://pkg.go.dev/github.com/cyphar/filepath-securejoin#section-readme
      cwe:
        - "CWE-22: Improper Limitation of a Pathname to a Restricted Directory
          ('Path Traversal')"
      owasp:
        - A05:2017 - Broken Access Control
        - A01:2021 - Broken Access Control
      category: security
      technology:
        - go
      cwe2022-top25: true
      cwe2021-top25: true
      subcategory:
        - vuln
      likelihood: MEDIUM
      impact: MEDIUM
      confidence: MEDIUM
      interfile: true
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
      vulnerability_class:
        - Path Traversal

Examples

filepath-clean-misuse.go

package main

import (
	"io/ioutil"
	"log"
	"net/http"
	"path/filepath"
	"path"
	"strings"
)

const root = "/tmp"

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/bad1", func(w http.ResponseWriter, r *http.Request) {
		// ruleid: filepath-clean-misuse
		filename := filepath.Clean(r.URL.Path)
		filename := filepath.Join(root, strings.Trim(filename, "/"))
		contents, err := ioutil.ReadFile(filename)
		if err != nil {
			w.WriteHeader(http.StatusNotFound)
			return
		}
		w.Write(contents)
	})

	mux.HandleFunc("/bad2", func(w http.ResponseWriter, r *http.Request) {
		// ruleid: filepath-clean-misuse
		filename := path.Clean(r.URL.Path)
		filename := filepath.Join(root, strings.Trim(filename, "/"))
		contents, err := ioutil.ReadFile(filename)
		if err != nil {
			w.WriteHeader(http.StatusNotFound)
			return
		}
		w.Write(contents)
	})

	mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) {
		filename := r.URL.Path
		// ok: filepath-clean-misuse
		filename := filepath.Join(root, strings.Trim(filename, "/"))
		contents, err := ioutil.ReadFile(filename)
		if err != nil {
			w.WriteHeader(http.StatusNotFound)
			return
		}
		w.Write(contents)
	})

	mux.HandleFunc("/ok2", func(w http.ResponseWriter, r *http.Request) {
		// ok: filepath-clean-misuse
		filename := path.Clean("/" + r.URL.Path)
		filename := filepath.Join(root, strings.Trim(filename, "/"))
		contents, err := ioutil.ReadFile(filename)
		if err != nil {
			w.WriteHeader(http.StatusNotFound)
			return
		}
		w.Write(contents)
	})

	server := &http.Server{
		Addr:    "127.0.0.1:50000",
		Handler: mux,
	}

	log.Fatal(server.ListenAndServe())
}

// TODO
// func NewHandlerWithDefault(root http.FileSystem, handler http.Handler, defaultPath string, gatewayDomains []string) http.Handler {
// 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 		if isGatewayRequest(r) {
// 			// s3 signed request reaching the ui handler, return an error response instead of the default path
// 			o := operations.Operation{}
// 			err := errors.Codes[errors.ERRLakeFSWrongEndpoint]
// 			err.Description = fmt.Sprintf("%s (%v)", err.Description, gatewayDomains)
// 			o.EncodeError(w, r, err)
// 			return
// 		}
// 		urlPath := r.URL.Path
// 		// We want this rule to only fire when urlPath does not have
// 		// a slash in front. This if condition ensures there is a slash,
// 		// so the line marked 'ok' below should not fire.
// 		if !strings.HasPrefix(urlPath, "/") {
// 			urlPath = "/" + urlPath
// 			r.URL.Path = urlPath
// 		}
// 		// ok: filepath-clean-misuse
// 		_, err := root.Open(path.Clean(urlPath))
// 		if err != nil && os.IsNotExist(err) {
// 			r.URL.Path = defaultPath
// 		}
// 		// consistent content-type
// 		contentType := gomime.TypeByExtension(filepath.Ext(r.URL.Path))
// 		if contentType != "" {
// 			w.Header().Set("Content-Type", contentType)
// 		}
// 		handler.ServeHTTP(w, r)
// 	})
// }