From 16bbcd3b81c754b45719aae354ac543ae84bf078 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Sat, 4 Oct 2025 16:19:44 +0200 Subject: [PATCH] replace securejoin with os.openroot Signed-off-by: Ronni Skansing --- backend/app/server.go | 121 +++-- backend/controller/asset.go | 34 +- backend/go.mod | 1 - backend/go.sum | 2 - backend/service/asset.go | 181 +++++-- backend/service/attachment.go | 107 +++-- backend/service/domain.go | 59 ++- backend/service/file.go | 188 +++++++- backend/service/import.go | 32 +- .../cyphar/filepath-securejoin/CHANGELOG.md | 178 ------- .../cyphar/filepath-securejoin/LICENSE | 28 -- .../cyphar/filepath-securejoin/README.md | 169 ------- .../cyphar/filepath-securejoin/VERSION | 1 - .../cyphar/filepath-securejoin/doc.go | 39 -- .../cyphar/filepath-securejoin/join.go | 125 ----- .../filepath-securejoin/lookup_linux.go | 389 ---------------- .../cyphar/filepath-securejoin/mkdir_linux.go | 207 -------- .../cyphar/filepath-securejoin/open_linux.go | 103 ---- .../filepath-securejoin/openat2_linux.go | 128 ----- .../filepath-securejoin/openat_linux.go | 59 --- .../filepath-securejoin/procfs_linux.go | 440 ------------------ .../cyphar/filepath-securejoin/vfs.go | 35 -- backend/vendor/modules.txt | 3 - 23 files changed, 571 insertions(+), 2058 deletions(-) delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/LICENSE delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/README.md delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/VERSION delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/doc.go delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/join.go delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/lookup_linux.go delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/mkdir_linux.go delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/open_linux.go delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/openat2_linux.go delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/openat_linux.go delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/procfs_linux.go delete mode 100644 backend/vendor/github.com/cyphar/filepath-securejoin/vfs.go diff --git a/backend/app/server.go b/backend/app/server.go index 8a59fe5..90fca3b 100644 --- a/backend/app/server.go +++ b/backend/app/server.go @@ -20,7 +20,7 @@ import ( "gopkg.in/yaml.v3" "github.com/caddyserver/certmagic" - securejoin "github.com/cyphar/filepath-securejoin" + "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/phishingclub/phishingclub/cache" @@ -202,26 +202,46 @@ func (s *Server) testTCPConnection(identifier string, addr string) chan server.S // and serves it if it is // return true if the request was for static content func (s *Server) checkAndServeAssets(c *gin.Context, host string) bool { - // check if the path is a file - staticPath, err := securejoin.SecureJoin(s.staticPath, host) + // create root filesystem for asset validation + root, err := os.OpenRoot(s.staticPath) if err != nil { - s.logger.Infow("insecure path attempted on asset", + s.logger.Infow("failed to open static path root", "error", err, ) return false } - staticPath, err = securejoin.SecureJoin(staticPath, c.Request.URL.Path) - if err != nil { - s.logger.Infow("insecure path attempted on asset", + defer root.Close() + + // validate host folder path is safe + _, err = root.Stat(host) + if err != nil && !os.IsNotExist(err) { + s.logger.Infow("insecure host path attempted", + "host", host, "error", err, ) return false } - // check if path exists on the specific domain - info, err := os.Stat(staticPath) + + // clean path and remove leading slash + cleanPath := strings.TrimPrefix(filepath.Clean(c.Request.URL.Path), "/") + + // validate full path is safe by checking it against root + fullRelativePath := filepath.Join(host, cleanPath) + _, err = root.Stat(fullRelativePath) + if err != nil && !os.IsNotExist(err) { + s.logger.Infow("insecure path attempted on asset", + "path", fullRelativePath, + "error", err, + ) + return false + } + + // check if file exists and get info through root + fullRelativePathForFile := filepath.Join(host, cleanPath) + info, err := root.Stat(fullRelativePathForFile) if err != nil { s.logger.Debugw("not found on domain: %s", - "path", staticPath, + "path", fullRelativePathForFile, ) // check if this is a global asset return s.checkAndServeSharedAsset(c) @@ -229,37 +249,80 @@ func (s *Server) checkAndServeAssets(c *gin.Context, host string) bool { if info.IsDir() { return false } - // checks if the path is a directory - c.Header("Content-Type", mime.TypeByExtension(filepath.Ext(staticPath))) - c.File(staticPath) - return true -} -func (s *Server) checkAndServeSharedAsset(c *gin.Context) bool { - // check if the path is a file - // TODO I need to somehow make this safe from directory traversal - staticPath, err := securejoin.SecureJoin( - s.staticPath+"/shared", - c.Request.URL.Path, - ) + // open and serve file through root to maintain security boundaries + file, err := root.Open(fullRelativePathForFile) if err != nil { - s.logger.Infow("insecure path attempted on asset", + s.logger.Infow("failed to open file through root", + "path", fullRelativePathForFile, "error", err, ) return false } - // check if path exists - info, err := os.Stat(staticPath) + defer file.Close() + + c.Header("Content-Type", mime.TypeByExtension(filepath.Ext(cleanPath))) + c.DataFromReader(http.StatusOK, info.Size(), mime.TypeByExtension(filepath.Ext(cleanPath)), file, nil) + return true +} + +func (s *Server) checkAndServeSharedAsset(c *gin.Context) bool { + // create root filesystem for secure shared asset validation + root, err := os.OpenRoot(s.staticPath) + if err != nil { + s.logger.Infow("failed to open static path root", + "error", err, + ) + return false + } + defer root.Close() + + // validate shared folder path is safe + _, err = root.Stat("shared") + if err != nil && !os.IsNotExist(err) { + s.logger.Infow("insecure shared path", + "error", err, + ) + return false + } + + // clean path and remove leading slash + cleanPath := strings.TrimPrefix(filepath.Clean(c.Request.URL.Path), "/") + + // validate full path is safe by checking it against root + fullRelativePath := filepath.Join("shared", cleanPath) + _, err = root.Stat(fullRelativePath) + if err != nil && !os.IsNotExist(err) { + s.logger.Infow("insecure shared asset path", + "path", fullRelativePath, + "error", err, + ) + return false + } + + // check if file exists and get info through root + sharedRelativePath := filepath.Join("shared", cleanPath) + info, err := root.Stat(sharedRelativePath) if err != nil { - _ = err return false } if info.IsDir() { return false } - // checks if the path is a directory - c.Header("Content-Type", mime.TypeByExtension(filepath.Ext(staticPath))) - c.File(staticPath) + + // open and serve file through root to maintain security boundaries + file, err := root.Open(sharedRelativePath) + if err != nil { + s.logger.Infow("failed to open shared file through root", + "path", sharedRelativePath, + "error", err, + ) + return false + } + defer file.Close() + + c.Header("Content-Type", mime.TypeByExtension(filepath.Ext(cleanPath))) + c.DataFromReader(http.StatusOK, info.Size(), mime.TypeByExtension(filepath.Ext(cleanPath)), file, nil) return true } diff --git a/backend/controller/asset.go b/backend/controller/asset.go index 3636490..95d667e 100644 --- a/backend/controller/asset.go +++ b/backend/controller/asset.go @@ -8,10 +8,10 @@ import ( "net/url" "os" "path/filepath" + "strings" "github.com/go-errors/errors" - securejoin "github.com/cyphar/filepath-securejoin" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/oapi-codegen/nullable" @@ -78,15 +78,30 @@ func (a *Asset) GetContentByID(g *gin.Context) { // such as the company name or something that is prefixed _ = data.ASSET_GLOBAL_FOLDER } - staticPath, err := securejoin.SecureJoin(a.StaticAssetPath, domain.String()) + // create root filesystem for secure asset access + root, err := os.OpenRoot(a.StaticAssetPath) if err != nil { - a.Logger.Debugw("insecure path", + a.Logger.Debugw("failed to open static asset path root", "path", a.StaticAssetPath, + "error", err, + ) + a.Response.ServerError(g) + return + } + defer root.Close() + + // validate domain directory access + domainRoot, err := root.OpenRoot(domain.String()) + if err != nil { + a.Logger.Debugw("insecure domain path", "domain", domain.String(), "error", err, ) + a.Response.BadRequest(g) return } + defer domainRoot.Close() + // get the file path pathDecoded, err := url.QueryUnescape(g.Param("path")) if err != nil { @@ -97,15 +112,22 @@ func (a *Asset) GetContentByID(g *gin.Context) { return } - filePath, err := securejoin.SecureJoin(staticPath, pathDecoded) + // clean path and remove leading slash for os.OpenRoot compatibility + cleanPath := strings.TrimPrefix(filepath.Clean(pathDecoded), "/") + + // validate file path within domain directory + _, err = domainRoot.Stat(cleanPath) if err != nil { - a.Logger.Debugw("insecure path", - "path", pathDecoded, + a.Logger.Debugw("insecure file path", + "path", cleanPath, "error", err, ) a.Response.BadRequest(g) return } + + // build safe file path (validated by OpenRoot) + filePath := filepath.Join(a.StaticAssetPath, domain.String(), cleanPath) // check if the file exists a.Logger.Debugw("checking if asset exists", "path", filePath, diff --git a/backend/go.mod b/backend/go.mod index 3aefa65..65cf887 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,7 +10,6 @@ require ( github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbletea v1.3.4 github.com/charmbracelet/lipgloss v1.1.0 - github.com/cyphar/filepath-securejoin v0.3.4 github.com/fatih/color v1.15.0 github.com/gin-contrib/zap v1.1.4 github.com/gin-gonic/gin v1.10.0 diff --git a/backend/go.sum b/backend/go.sum index 00ae711..bac5a87 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -34,8 +34,6 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= -github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/backend/service/asset.go b/backend/service/asset.go index 3bf4b54..1f4c4cc 100644 --- a/backend/service/asset.go +++ b/backend/service/asset.go @@ -8,7 +8,8 @@ import ( "github.com/go-errors/errors" - securejoin "github.com/cyphar/filepath-securejoin" + "os" + "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/oapi-codegen/nullable" @@ -62,7 +63,7 @@ func (a *Asset) Create( contextFolder, ) - files := []*FileUpload{} + files := []*RootFileUpload{} for _, asset := range assets { domainNameProvided := asset.DomainName.IsSpecified() && !asset.DomainName.IsNull() // ensure context is the same across all files @@ -105,16 +106,10 @@ func (a *Asset) Create( } path = p } - path, err = securejoin.SecureJoin(path, asset.File.Filename) - if err != nil { - a.Logger.Warnw("insecure path", "error", err) - return ids, validate.WrapErrorWithField( - errs.NewValidationError(err), - "Path", - ) - } + // build full relative path including filename for DB storage + fullRelativePath := filepath.Join(path, asset.File.Filename) // relative path is used in the DB - relativePath, err := vo.NewRelativeFilePath(path) + relativePath, err := vo.NewRelativeFilePath(fullRelativePath) if err != nil { a.Logger.Debugw("failed to make file path", "error", err) return ids, validate.WrapErrorWithField( @@ -155,29 +150,78 @@ func (a *Asset) Create( // this is a bit dirty, but I will do it anyway // overwriting the path the client assigned with the context relative path including the file name asset.Path = nullable.NewNullableWithValue(*relativePath) - // full path is used in the file system - pathWithRootAndDomainContext, err := securejoin.SecureJoin(a.RootFolder, contextFolder) - if err != nil { - a.Logger.Debugw("insecure path", "path", err) - return ids, fmt.Errorf("insecure path: %s", err) + // ensure base asset directory exists + if err := os.MkdirAll(a.RootFolder, 0755); err != nil { + a.Logger.Debugw("failed to create asset root directory", "error", err) + return ids, fmt.Errorf("failed to create asset root directory: %s", err) } - pathWithRootAndDomainContext, err = securejoin.SecureJoin( - pathWithRootAndDomainContext, - path, - ) - if err != nil { - a.Logger.Debugw("insecure path", "error", err) - return ids, fmt.Errorf("insecure path: %s", err) - } - a.Logger.Debugw("file path", - "relative", relativePath.String(), - "relativeWithRootPath", pathWithRootAndDomainContext, - ) - asset.Path = nullable.NewNullableWithValue(*relativePath) // path to file from within the context - files = append(files, NewFileUpload(pathWithRootAndDomainContext, &asset.File)) + // create root filesystem for the full context path (controlled paths only) + fullContextPath := filepath.Join(a.RootFolder, contextFolder) + contextRoot, err := os.OpenRoot(fullContextPath) + var isUsingParentRoot bool + if err != nil { + // context path doesn't exist - this is OK for uploads, directories will be created + a.Logger.Debugw("context path doesn't exist yet", "path", fullContextPath, "error", err) + // for validation purposes, we'll use the parent root and validate the context is safe + parentRoot, parentErr := os.OpenRoot(a.RootFolder) + if parentErr != nil { + a.Logger.Debugw("failed to open root folder", "error", parentErr) + return ids, fmt.Errorf("failed to open root folder: %s", parentErr) + } + defer parentRoot.Close() + + // validate context folder is safe (doesn't need to exist) + _, statErr := parentRoot.Stat(contextFolder) + if statErr != nil && !os.IsNotExist(statErr) { + a.Logger.Debugw("invalid context folder", "error", statErr) + return ids, fmt.Errorf("invalid context folder: %s", statErr) + } + contextRoot = parentRoot + isUsingParentRoot = true + } else { + defer contextRoot.Close() + } + + // build and validate full user path through OpenRoot + var fullUserPath string + if path != "" { + fullUserPath = filepath.Join(strings.Trim(path, "/"), asset.File.Filename) + } else { + fullUserPath = asset.File.Filename + } + + // validate full path is safe (doesn't need to exist) + _, err = contextRoot.Stat(fullUserPath) + if err != nil && !os.IsNotExist(err) { + a.Logger.Debugw("invalid file path", "path", fullUserPath, "error", err) + return ids, fmt.Errorf("invalid file path: %s", err) + } + + // build relative path for secure upload + var uploadRelativePath string + if path != "" { + uploadRelativePath = filepath.Join(strings.Trim(path, "/"), asset.File.Filename) + } else { + uploadRelativePath = asset.File.Filename + } + + // if using parent root, we need to include context folder in path + var pathToValidate string + if isUsingParentRoot { + pathToValidate = filepath.Join(contextFolder, uploadRelativePath) + } else { + pathToValidate = uploadRelativePath + } + + a.Logger.Debugw("secure file path", + "contextPath", fullContextPath, + "relativePath", pathToValidate, + ) + + files = append(files, NewRootFileUpload(contextRoot, pathToValidate, &asset.File)) } - // upload files to the file system + // upload files to the file system using secure method _, err = a.FileService.Upload( g, files, @@ -423,16 +467,31 @@ func (a *Asset) DeleteByID( return err } - domainRoot, err := securejoin.SecureJoin(a.RootFolder, domainContext) + // create root filesystem for secure deletion + root, err := os.OpenRoot(a.RootFolder) if err != nil { - a.Logger.Debugw("insecure path", "error", err) + a.Logger.Debugw("failed to open root folder", "error", err) return err } - filePath, err := securejoin.SecureJoin(domainRoot, p.String()) + defer root.Close() + + // validate domain context access + domainRoot, err := root.OpenRoot(domainContext) if err != nil { - a.Logger.Debugw("insecure path", "error", err) + a.Logger.Debugw("failed to open domain context", "error", err) return err } + defer domainRoot.Close() + + // validate file exists within domain context + _, err = domainRoot.Stat(p.String()) + if err != nil { + a.Logger.Debugw("file not found in domain context", "error", err) + return err + } + + // build safe file path (validated by OpenRoot) + filePath := filepath.Join(a.RootFolder, domainContext, p.String()) err = a.FileService.Delete( filePath, @@ -445,7 +504,7 @@ func (a *Asset) DeleteByID( return err } err = a.FileService.RemoveEmptyFolderRecursively( - domainRoot, + filepath.Join(a.RootFolder, domainContext), filepath.Dir(filePath), ) if err != nil { @@ -512,16 +571,31 @@ func (a *Asset) DeleteAllByCompanyID( a.Logger.Debugw("failed to get path", "error", err) return err } - domainRoot, err := securejoin.SecureJoin(a.RootFolder, domainContext) + // create root filesystem for secure deletion + root, err := os.OpenRoot(a.RootFolder) if err != nil { - a.Logger.Debugw("insecure path", "error", err) + a.Logger.Debugw("failed to open root folder", "error", err) return err } - filePath, err := securejoin.SecureJoin(domainRoot, p.String()) + defer root.Close() + + // validate domain context access + domainContextRoot, err := root.OpenRoot(domainContext) if err != nil { - a.Logger.Debugw("insecure path", "error", err) + a.Logger.Debugw("failed to open domain context", "error", err) return err } + defer domainContextRoot.Close() + + // validate file exists within domain context + _, err = domainContextRoot.Stat(p.String()) + if err != nil { + a.Logger.Debugw("file not found in domain context", "error", err) + return err + } + + // build safe file path (validated by OpenRoot) + filePath := filepath.Join(a.RootFolder, domainContext, p.String()) err = a.FileService.Delete( filePath, ) @@ -533,7 +607,7 @@ func (a *Asset) DeleteAllByCompanyID( return err } err = a.FileService.RemoveEmptyFolderRecursively( - domainRoot, + filepath.Join(a.RootFolder, domainContext), filepath.Dir(filePath), ) if err != nil { @@ -608,16 +682,31 @@ func (a *Asset) DeleteAllByDomainID( return err } - domainRoot, err := securejoin.SecureJoin(a.RootFolder, domainContext) + // create root filesystem for secure deletion + root, err := os.OpenRoot(a.RootFolder) if err != nil { - a.Logger.Debugw("insecure path", "error", err) + a.Logger.Debugw("failed to open root folder", "error", err) return err } - filePath, err := securejoin.SecureJoin(domainRoot, p.String()) + defer root.Close() + + // validate domain context access + domainContextRoot, err := root.OpenRoot(domainContext) if err != nil { - a.Logger.Debugw("insecure path", "error", err) + a.Logger.Debugw("failed to open domain context", "error", err) return err } + defer domainContextRoot.Close() + + // validate file exists within domain context + _, err = domainContextRoot.Stat(p.String()) + if err != nil { + a.Logger.Debugw("file not found in domain context", "error", err) + return err + } + + // build safe file path (validated by OpenRoot) + filePath := filepath.Join(a.RootFolder, domainContext, p.String()) err = a.FileService.Delete( filePath, ) @@ -629,7 +718,7 @@ func (a *Asset) DeleteAllByDomainID( return err } err = a.FileService.RemoveEmptyFolderRecursively( - domainRoot, + filepath.Join(a.RootFolder, domainContext), filepath.Dir(filePath), ) if err != nil { diff --git a/backend/service/attachment.go b/backend/service/attachment.go index e276c1d..3c6a03a 100644 --- a/backend/service/attachment.go +++ b/backend/service/attachment.go @@ -3,11 +3,11 @@ package service import ( "context" "fmt" + "os" "path/filepath" "github.com/go-errors/errors" - securejoin "github.com/cyphar/filepath-securejoin" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/oapi-codegen/nullable" @@ -59,7 +59,7 @@ func (a *Attachment) Create( "all attachments must have the same context '%s'", contextFolder, ) - files := []*FileUpload{} + files := []*RootFileUpload{} filePaths := []string{} for _, attachment := range attachments { // ensure context is the same across all files @@ -94,31 +94,40 @@ func (a *Attachment) Create( ) return createdIDs, errs.Wrap(err) } - // full path is used in the file system - pathWithRootAndDomainContext, err := securejoin.SecureJoin( - a.RootFolder, - contextFolder, - ) - if err != nil { - a.Logger.Infow("insecure path", "error", err) + // ensure base attachment directory exists + if err := os.MkdirAll(a.RootFolder, 0755); err != nil { + a.Logger.Debugw("failed to create attachment root directory", "error", err) return createdIDs, errs.Wrap(err) } - pathWithRootAndDomainContext, err = securejoin.SecureJoin( - pathWithRootAndDomainContext, - attachmentFilename, - ) + + // create root filesystem for the full context path (controlled paths only) + fullContextPath := filepath.Join(a.RootFolder, contextFolder) + contextRoot, err := os.OpenRoot(fullContextPath) if err != nil { - a.Logger.Infow("insecure path", "error", err) + a.Logger.Infow("failed to open context folder", "error", err) return createdIDs, errs.Wrap(err) } - a.Logger.Debugw("file path: %s", + defer contextRoot.Close() + + // validate that the filename is accessible within the context + // this prevents directory traversal in the filename itself + _, err = contextRoot.Stat(attachmentFilename) + if err != nil && !os.IsNotExist(err) { + a.Logger.Infow("invalid filename", "filename", attachmentFilename, "error", err) + return createdIDs, errs.Wrap(err) + } + + // build final path for logging + pathWithRootAndDomainContext := filepath.Join(fullContextPath, attachmentFilename) + a.Logger.Debugw("secure file path", "relative", relativePath.String(), - "relativeWithRootFilePath", pathWithRootAndDomainContext, + "contextPath", fullContextPath, + "filename", attachmentFilename, ) filePaths = append(filePaths, pathWithRootAndDomainContext) - files = append(files, NewFileUpload(pathWithRootAndDomainContext, attachment.File)) + files = append(files, NewRootFileUpload(contextRoot, attachmentFilename, attachment.File)) } - // upload files to the file system + // upload files to the file system using secure method _, err = a.FileService.Upload( g, files, @@ -342,18 +351,33 @@ func (a *Attachment) DeleteByID( if attachment.CompanyID.IsSpecified() && !attachment.CompanyID.IsNull() { companyContext = attachment.CompanyID.MustGet().String() } - domainRoot, err := securejoin.SecureJoin(a.RootFolder, companyContext) + // create root filesystem for secure file operations + root, err := os.OpenRoot(a.RootFolder) if err != nil { - a.Logger.Debugw("insecure path", "error", err) + a.Logger.Debugw("failed to open root folder", "error", err) return err } - attachmentFileName := attachment.FileName.MustGet() - filename, err := securejoin.SecureJoin(domainRoot, attachmentFileName.String()) - if err != nil { + defer root.Close() - a.Logger.Debugw("insecure path", "error", err) + // validate that company context exists and is accessible + companyRoot, err := root.OpenRoot(companyContext) + if err != nil { + a.Logger.Debugw("failed to open company context", "error", err) return err } + defer companyRoot.Close() + + attachmentFileName := attachment.FileName.MustGet() + + // validate that the file exists and is accessible within the company context + _, err = companyRoot.Stat(attachmentFileName.String()) + if err != nil { + a.Logger.Debugw("attachment file not found in context", "error", err) + return err + } + + // build full path for file service (validated as safe by OpenRoot) + filename := filepath.Join(a.RootFolder, companyContext, attachmentFileName.String()) err = a.FileService.Delete( filename, ) @@ -387,28 +411,31 @@ func (a *Attachment) GetPath(attachment *model.Attachment) (*vo.RelativeFilePath } // map attachments to files attachmentFilename := filepath.Clean(attachment.FileName.MustGet().String()) - // relative path is used in the DB - /* - relativePath, err := vo.NewRelativeFilePath(attachmentFilename) - if err != nil { - a.Logger.Debugw("failed to make file path", err) - return nil,errs.Wrap(err) - } - */ - // full path is used in the file system - pathWithRootAndDomainContext, err := securejoin.SecureJoin(a.RootFolder, contextFolder) + // create root filesystem for secure path operations + root, err := os.OpenRoot(a.RootFolder) if err != nil { - a.Logger.Infow("insecure path", "error", err) + a.Logger.Infow("failed to open root folder", "error", err) return nil, errs.Wrap(err) } - pathWithRootAndDomainContext, err = securejoin.SecureJoin( - pathWithRootAndDomainContext, - attachmentFilename, - ) + defer root.Close() + + // validate that the context folder and filename are accessible within root + contextRoot, err := root.OpenRoot(contextFolder) if err != nil { - a.Logger.Infow("insecure path", "error", err) + a.Logger.Infow("failed to open context folder", "error", err) return nil, errs.Wrap(err) } + defer contextRoot.Close() + + // verify the file exists and is accessible + _, err = contextRoot.Stat(attachmentFilename) + if err != nil { + a.Logger.Debugw("file not accessible", "error", err) + return nil, errs.Wrap(err) + } + + // build full path for return value + pathWithRootAndDomainContext := filepath.Join(a.RootFolder, contextFolder, attachmentFilename) path, err := vo.NewRelativeFilePath(pathWithRootAndDomainContext) if err != nil { a.Logger.Debugw("failed to make file path", "error", err) diff --git a/backend/service/domain.go b/backend/service/domain.go index 2ce4e1a..5a13511 100644 --- a/backend/service/domain.go +++ b/backend/service/domain.go @@ -7,13 +7,14 @@ import ( "fmt" "net" "net/http" + "os" + "path/filepath" "strings" "time" "github.com/go-errors/errors" "github.com/caddyserver/certmagic" - securejoin "github.com/cyphar/filepath-securejoin" "github.com/google/uuid" "github.com/oapi-codegen/nullable" "github.com/phishingclub/phishingclub/build" @@ -783,13 +784,26 @@ func (d *Domain) handleOwnManagedTLS( if len(key) > 0 && len(pem) > 0 { keyBuffer := bytes.NewBufferString(key) pemBuffer := bytes.NewBufferString(pem) - path, err := securejoin.SecureJoin(d.OwnManagedCertificatePath, name) + + // create root filesystem for secure certificate operations + root, err := os.OpenRoot(d.OwnManagedCertificatePath) if err != nil { - return nil, fmt.Errorf("failed to join cert path and domain name: %s", err) + return nil, fmt.Errorf("failed to open certificate path: %s", err) } + defer root.Close() + + // validate domain name directory access + _, err = root.Stat(name) + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("invalid domain name for certificate path: %s", err) + } + + // build path for certificate operations + // build safe path for certificate operations (validated by OpenRoot) + // use secure file upload for certificate operations err = d.FileService.UploadFile( - ctx, - path+"/cert.key", + root, + name+"/cert.key", keyBuffer, true, ) @@ -801,8 +815,8 @@ func (d *Domain) handleOwnManagedTLS( return nil, errs.Wrap(err) } err = d.FileService.UploadFile( - ctx, - path+"/cert.pem", + root, + name+"/cert.pem", pemBuffer, true, ) @@ -813,13 +827,14 @@ func (d *Domain) handleOwnManagedTLS( ) return nil, errs.Wrap(err) } - keyBuffer = bytes.NewBufferString(key) - pemBuffer = bytes.NewBufferString(pem) + // Create fresh buffers for caching since upload consumed the original buffers + keyBufferForCache := bytes.NewBufferString(key) + pemBufferForCache := bytes.NewBufferString(pem) hash, err := d.CertMagicConfig.CacheUnmanagedCertificatePEMBytes( ctx, - pemBuffer.Bytes(), - keyBuffer.Bytes(), - []string{}, + pemBufferForCache.Bytes(), + keyBufferForCache.Bytes(), + []string{name}, ) if err != nil { d.Logger.Errorw( @@ -844,10 +859,26 @@ func (d *Domain) removeOwnManagedTLS( domain *model.Domain, ) error { name := domain.Name.MustGet().String() - path, err := securejoin.SecureJoin(d.OwnManagedCertificatePath, name) + + // create root filesystem for secure certificate operations + root, err := os.OpenRoot(d.OwnManagedCertificatePath) if err != nil { - return fmt.Errorf("failed to delete own managed TLS for '%s' as: %s", name, err) + return fmt.Errorf("failed to open certificate path for '%s': %s", name, err) } + defer root.Close() + + // validate domain name directory exists + _, err = root.Stat(name) + if err != nil { + if os.IsNotExist(err) { + // directory doesn't exist, nothing to delete + return nil + } + return fmt.Errorf("failed to access certificate directory for '%s': %s", name, err) + } + + // build safe path for deletion (validated by OpenRoot) + path := filepath.Join(d.OwnManagedCertificatePath, name) err = d.FileService.DeleteAll(path) if err != nil { return fmt.Errorf("failed to delete own managed TLS for '%s' as: %s", name, err) diff --git a/backend/service/file.go b/backend/service/file.go index 8596b53..dad163d 100644 --- a/backend/service/file.go +++ b/backend/service/file.go @@ -4,15 +4,15 @@ import ( "bytes" "context" "fmt" + "io" "io/fs" "mime/multipart" "os" "path/filepath" "strings" - "github.com/go-errors/errors" - "github.com/gin-gonic/gin" + "github.com/go-errors/errors" "github.com/phishingclub/phishingclub/errs" "github.com/phishingclub/phishingclub/validate" ) @@ -23,6 +23,13 @@ type FileUpload struct { File *multipart.FileHeader } +// RootFileUpload is a file upload using os.Root +type RootFileUpload struct { + Root *os.Root + RelativePath string + File *multipart.FileHeader +} + // NewFileUpload creates a new file upload func NewFileUpload(path string, file *multipart.FileHeader) *FileUpload { return &FileUpload{ @@ -31,6 +38,15 @@ func NewFileUpload(path string, file *multipart.FileHeader) *FileUpload { } } +// NewRootFileUpload creates a new secure file upload +func NewRootFileUpload(root *os.Root, relativePath string, file *multipart.FileHeader) *RootFileUpload { + return &RootFileUpload{ + Root: root, + RelativePath: relativePath, + File: file, + } +} + // File is a File service type File struct { Common @@ -63,7 +79,7 @@ func (f *File) checkFilePathIsValidForUpload(path string) error { return nil } -func (f *File) Upload( +func (f *File) UploadLegacy( g *gin.Context, files []*FileUpload, ) (int, error) { @@ -108,7 +124,7 @@ func (f *File) Upload( return len(files), nil } -func (f *File) UploadFile( +func (f *File) UploadFileLegacy( ctx context.Context, path string, contents *bytes.Buffer, @@ -235,3 +251,167 @@ func (f *File) RemoveEmptyFolderRecursively( parent := filepath.Dir(path) return f.RemoveEmptyFolderRecursively(rootPath, parent) } + +// Upload uploads files using os.Root for security +func (f *File) Upload( + g *gin.Context, + files []*RootFileUpload, +) (int, error) { + for _, fileUpload := range files { + root := fileUpload.Root + relativePath := fileUpload.RelativePath + file := fileUpload.File + + f.Logger.Debugw("checking if file path is valid", "path", relativePath) + + // validate path is safe through root + _, err := root.Stat(relativePath) + if err != nil && !os.IsNotExist(err) { + f.Logger.Errorw("invalid file path", "path", relativePath, "error", err) + return 0, errs.Wrap(err) + } + + // check if file already exists + pathDoesNotExist := os.IsNotExist(err) + if !pathDoesNotExist { + f.Logger.Debugw("file already exists", "path", relativePath) + return 0, errs.NewValidationError(fmt.Errorf("file already exists: %s", relativePath)) + } + + // create directory structure if needed + dirPath := filepath.Dir(relativePath) + if dirPath != "." { + // create directories step by step + parts := strings.Split(dirPath, "/") + currentPath := "" + for _, part := range parts { + if part != "" { + if currentPath == "" { + currentPath = part + } else { + currentPath = filepath.Join(currentPath, part) + } + + // check if directory exists + _, err = root.Stat(currentPath) + if err != nil && !os.IsNotExist(err) { + f.Logger.Errorw("invalid directory path", "path", currentPath, "error", err) + return 0, errs.Wrap(err) + } + + // create directory if it doesn't exist + if os.IsNotExist(err) { + err = root.Mkdir(currentPath, 0755) + if err != nil { + f.Logger.Errorw("failed to create directory", "path", currentPath, "error", err) + return 0, errs.Wrap(err) + } + f.Logger.Debugw("created directory", "path", currentPath) + } + } + } + } + + // open file for writing through root + dst, err := root.OpenFile(relativePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + f.Logger.Errorw("failed to create file", "path", relativePath, "error", err) + return 0, errs.Wrap(err) + } + defer dst.Close() + + // open uploaded file + src, err := file.Open() + if err != nil { + f.Logger.Errorw("failed to open uploaded file", "error", err) + return 0, errs.Wrap(err) + } + defer src.Close() + + // copy file content + _, err = io.Copy(dst, src) + if err != nil { + f.Logger.Errorw("failed to copy file content", "error", err) + return 0, errs.Wrap(err) + } + + f.Logger.Debugw("file uploaded successfully", "path", relativePath) + } + return len(files), nil +} + +// UploadFile uploads a file using os.Root for security +func (f *File) UploadFile( + root *os.Root, + relativePath string, + contents *bytes.Buffer, + overwrite bool, +) error { + f.Logger.Debugw("checking if file path is valid", "path", relativePath) + + // validate path is safe through root + _, err := root.Stat(relativePath) + if err != nil && !os.IsNotExist(err) { + f.Logger.Errorw("invalid file path", "path", relativePath, "error", err) + return errs.Wrap(err) + } + + // check if file exists + pathDoesNotExist := os.IsNotExist(err) + if !pathDoesNotExist && !overwrite { + f.Logger.Debugw("file already exists and overwrite is false", "path", relativePath) + return errs.NewValidationError(fmt.Errorf("file already exists: %s", relativePath)) + } + + // create directory structure if needed + dirPath := filepath.Dir(relativePath) + if dirPath != "." { + // create directories step by step + parts := strings.Split(dirPath, "/") + currentPath := "" + for _, part := range parts { + if part != "" { + if currentPath == "" { + currentPath = part + } else { + currentPath = filepath.Join(currentPath, part) + } + + // check if directory exists + _, err = root.Stat(currentPath) + if err != nil && !os.IsNotExist(err) { + f.Logger.Errorw("invalid directory path", "path", currentPath, "error", err) + return errs.Wrap(err) + } + + // create directory if it doesn't exist + if os.IsNotExist(err) { + err = root.Mkdir(currentPath, 0755) + if err != nil { + f.Logger.Errorw("failed to create directory", "path", currentPath, "error", err) + return errs.Wrap(err) + } + f.Logger.Debugw("created directory", "path", currentPath) + } + } + } + } + + // open file for writing through root + dst, err := root.OpenFile(relativePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + f.Logger.Errorw("failed to create file", "path", relativePath, "error", err) + return errs.Wrap(err) + } + defer dst.Close() + + // write content + _, err = io.Copy(dst, contents) + if err != nil { + f.Logger.Errorw("failed to write file content", "error", err) + return errs.Wrap(err) + } + + f.Logger.Debugw("file uploaded successfully", "path", relativePath) + return nil +} diff --git a/backend/service/import.go b/backend/service/import.go index db7fc88..af82232 100644 --- a/backend/service/import.go +++ b/backend/service/import.go @@ -6,12 +6,10 @@ import ( "fmt" "io" "mime/multipart" + "os" "path/filepath" "strings" - "gopkg.in/yaml.v3" - - securejoin "github.com/cyphar/filepath-securejoin" "github.com/gin-gonic/gin" "github.com/go-errors/errors" "github.com/google/uuid" @@ -21,6 +19,7 @@ import ( "github.com/phishingclub/phishingclub/model" "github.com/phishingclub/phishingclub/repository" "github.com/phishingclub/phishingclub/vo" + "gopkg.in/yaml.v3" "gorm.io/gorm" ) @@ -751,19 +750,28 @@ func (im *Import) createAssetFromZipFile( // Build the file system path - assets are always stored in shared folder contextFolder := "shared" - // Build full file path - pathWithRootAndDomainContext, err := securejoin.SecureJoin(im.Asset.RootFolder, contextFolder) - if err != nil { - return false, err - } - pathWithRootAndDomainContext, err = securejoin.SecureJoin(pathWithRootAndDomainContext, fullRelativePath) - if err != nil { + // ensure base asset directory exists + if err := os.MkdirAll(im.Asset.RootFolder, 0755); err != nil { return false, err } - // Upload the file content directly + // create root filesystem for the full context path (controlled paths only) + fullContextPath := filepath.Join(im.Asset.RootFolder, contextFolder) + contextRoot, err := os.OpenRoot(fullContextPath) + if err != nil { + return false, err + } + defer contextRoot.Close() + + // validate file path within context + _, err = contextRoot.Stat(fullRelativePath) + if err != nil && !os.IsNotExist(err) { + return false, err + } + + // Upload the file content directly using secure method contentBuffer := bytes.NewBuffer(content) - err = im.File.UploadFile(g, pathWithRootAndDomainContext, contentBuffer, true) + err = im.File.UploadFile(contextRoot, strings.Trim(fullRelativePath, "/"), contentBuffer, true) if err != nil { // Clean up the database entry if file upload fails im.Asset.AssetRepository.DeleteByID(g, id) diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md b/backend/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md deleted file mode 100644 index 04b5685..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md +++ /dev/null @@ -1,178 +0,0 @@ -# Changelog # -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/) -and this project adheres to [Semantic Versioning](http://semver.org/). - -## [Unreleased] ## - -## [0.3.4] - 2024-10-09 ## - -### Fixed ### -- Previously, some testing mocks we had resulted in us doing `import "testing"` - in non-`_test.go` code, which made some downstreams like Kubernetes unhappy. - This has been fixed. (#32) - -## [0.3.3] - 2024-09-30 ## - -### Fixed ### -- The mode and owner verification logic in `MkdirAll` has been removed. This - was originally intended to protect against some theoretical attacks but upon - further consideration these protections don't actually buy us anything and - they were causing spurious errors with more complicated filesystem setups. -- The "is the created directory empty" logic in `MkdirAll` has also been - removed. This was not causing us issues yet, but some pseudofilesystems (such - as `cgroup`) create non-empty directories and so this logic would've been - wrong for such cases. - -## [0.3.2] - 2024-09-13 ## - -### Changed ### -- Passing the `S_ISUID` or `S_ISGID` modes to `MkdirAllInRoot` will now return - an explicit error saying that those bits are ignored by `mkdirat(2)`. In the - past a different error was returned, but since the silent ignoring behaviour - is codified in the man pages a more explicit error seems apt. While silently - ignoring these bits would be the most compatible option, it could lead to - users thinking their code sets these bits when it doesn't. Programs that need - to deal with compatibility can mask the bits themselves. (#23, #25) - -### Fixed ### -- If a directory has `S_ISGID` set, then all child directories will have - `S_ISGID` set when created and a different gid will be used for any inode - created under the directory. Previously, the "expected owner and mode" - validation in `securejoin.MkdirAll` did not correctly handle this. We now - correctly handle this case. (#24, #25) - -## [0.3.1] - 2024-07-23 ## - -### Changed ### -- By allowing `Open(at)InRoot` to opt-out of the extra work done by `MkdirAll` - to do the necessary "partial lookups", `Open(at)InRoot` now does less work - for both implementations (resulting in a many-fold decrease in the number of - operations for `openat2`, and a modest improvement for non-`openat2`) and is - far more guaranteed to match the correct `openat2(RESOLVE_IN_ROOT)` - behaviour. -- We now use `readlinkat(fd, "")` where possible. For `Open(at)InRoot` this - effectively just means that we no longer risk getting spurious errors during - rename races. However, for our hardened procfs handler, this in theory should - prevent mount attacks from tricking us when doing magic-link readlinks (even - when using the unsafe host `/proc` handle). Unfortunately `Reopen` is still - potentially vulnerable to those kinds of somewhat-esoteric attacks. - - Technically this [will only work on post-2.6.39 kernels][linux-readlinkat-emptypath] - but it seems incredibly unlikely anyone is using `filepath-securejoin` on a - pre-2011 kernel. - -### Fixed ### -- Several improvements were made to the errors returned by `Open(at)InRoot` and - `MkdirAll` when dealing with invalid paths under the emulated (ie. - non-`openat2`) implementation. Previously, some paths would return the wrong - error (`ENOENT` when the last component was a non-directory), and other paths - would be returned as though they were acceptable (trailing-slash components - after a non-directory would be ignored by `Open(at)InRoot`). - - These changes were done to match `openat2`'s behaviour and purely is a - consistency fix (most users are going to be using `openat2` anyway). - -[linux-readlinkat-emptypath]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=65cfc6722361570bfe255698d9cd4dccaf47570d - -## [0.3.0] - 2024-07-11 ## - -### Added ### -- A new set of `*os.File`-based APIs have been added. These are adapted from - [libpathrs][] and we strongly suggest using them if possible (as they provide - far more protection against attacks than `SecureJoin`): - - - `Open(at)InRoot` resolves a path inside a rootfs and returns an `*os.File` - handle to the path. Note that the handle returned is an `O_PATH` handle, - which cannot be used for reading or writing (as well as some other - operations -- [see open(2) for more details][open.2]) - - - `Reopen` takes an `O_PATH` file handle and safely re-opens it to upgrade - it to a regular handle. This can also be used with non-`O_PATH` handles, - but `O_PATH` is the most obvious application. - - - `MkdirAll` is an implementation of `os.MkdirAll` that is safe to use to - create a directory tree within a rootfs. - - As these are new APIs, they may change in the future. However, they should be - safe to start migrating to as we have extensive tests ensuring they behave - correctly and are safe against various races and other attacks. - -[libpathrs]: https://github.com/openSUSE/libpathrs -[open.2]: https://www.man7.org/linux/man-pages/man2/open.2.html - -## [0.2.5] - 2024-05-03 ## - -### Changed ### -- Some minor changes were made to how lexical components (like `..` and `.`) - are handled during path generation in `SecureJoin`. There is no behaviour - change as a result of this fix (the resulting paths are the same). - -### Fixed ### -- The error returned when we hit a symlink loop now references the correct - path. (#10) - -## [0.2.4] - 2023-09-06 ## - -### Security ### -- This release fixes a potential security issue in filepath-securejoin when - used on Windows ([GHSA-6xv5-86q9-7xr8][], which could be used to generate - paths outside of the provided rootfs in certain cases), as well as improving - the overall behaviour of filepath-securejoin when dealing with Windows paths - that contain volume names. Thanks to Paulo Gomes for discovering and fixing - these issues. - -### Fixed ### -- Switch to GitHub Actions for CI so we can test on Windows as well as Linux - and MacOS. - -[GHSA-6xv5-86q9-7xr8]: https://github.com/advisories/GHSA-6xv5-86q9-7xr8 - -## [0.2.3] - 2021-06-04 ## - -### Changed ### -- Switch to Go 1.13-style `%w` error wrapping, letting us drop the dependency - on `github.com/pkg/errors`. - -## [0.2.2] - 2018-09-05 ## - -### Changed ### -- Use `syscall.ELOOP` as the base error for symlink loops, rather than our own - (internal) error. This allows callers to more easily use `errors.Is` to check - for this case. - -## [0.2.1] - 2018-09-05 ## - -### Fixed ### -- Use our own `IsNotExist` implementation, which lets us handle `ENOTDIR` - properly within `SecureJoin`. - -## [0.2.0] - 2017-07-19 ## - -We now have 100% test coverage! - -### Added ### -- Add a `SecureJoinVFS` API that can be used for mocking (as we do in our new - tests) or for implementing custom handling of lookup operations (such as for - rootless containers, where work is necessary to access directories with weird - modes because we don't have `CAP_DAC_READ_SEARCH` or `CAP_DAC_OVERRIDE`). - -## 0.1.0 - 2017-07-19 - -This is our first release of `github.com/cyphar/filepath-securejoin`, -containing a full implementation with a coverage of 93.5% (the only missing -cases are the error cases, which are hard to mocktest at the moment). - -[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.4...HEAD -[0.3.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.3...v0.3.4 -[0.3.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.2...v0.3.3 -[0.3.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.1...v0.3.2 -[0.3.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.0...v0.3.1 -[0.3.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.5...v0.3.0 -[0.2.5]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.4...v0.2.5 -[0.2.4]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.3...v0.2.4 -[0.2.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.2...v0.2.3 -[0.2.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.1...v0.2.2 -[0.2.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.0...v0.2.1 -[0.2.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.1.0...v0.2.0 diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/LICENSE b/backend/vendor/github.com/cyphar/filepath-securejoin/LICENSE deleted file mode 100644 index cb1ab88..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. -Copyright (C) 2017-2024 SUSE LLC. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/README.md b/backend/vendor/github.com/cyphar/filepath-securejoin/README.md deleted file mode 100644 index eaeb53f..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/README.md +++ /dev/null @@ -1,169 +0,0 @@ -## `filepath-securejoin` ## - -[![Go Documentation](https://pkg.go.dev/badge/github.com/cyphar/filepath-securejoin.svg)](https://pkg.go.dev/github.com/cyphar/filepath-securejoin) -[![Build Status](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml/badge.svg)](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml) - -### Old API ### - -This library was originally just an implementation of `SecureJoin` which was -[intended to be included in the Go standard library][go#20126] as a safer -`filepath.Join` that would restrict the path lookup to be inside a root -directory. - -The implementation was based on code that existed in several container -runtimes. Unfortunately, this API is **fundamentally unsafe** against attackers -that can modify path components after `SecureJoin` returns and before the -caller uses the path, allowing for some fairly trivial TOCTOU attacks. - -`SecureJoin` (and `SecureJoinVFS`) are still provided by this library to -support legacy users, but new users are strongly suggested to avoid using -`SecureJoin` and instead use the [new api](#new-api) or switch to -[libpathrs][libpathrs]. - -With the above limitations in mind, this library guarantees the following: - -* If no error is set, the resulting string **must** be a child path of - `root` and will not contain any symlink path components (they will all be - expanded). - -* When expanding symlinks, all symlink path components **must** be resolved - relative to the provided root. In particular, this can be considered a - userspace implementation of how `chroot(2)` operates on file paths. Note that - these symlinks will **not** be expanded lexically (`filepath.Clean` is not - called on the input before processing). - -* Non-existent path components are unaffected by `SecureJoin` (similar to - `filepath.EvalSymlinks`'s semantics). - -* The returned path will always be `filepath.Clean`ed and thus not contain any - `..` components. - -A (trivial) implementation of this function on GNU/Linux systems could be done -with the following (note that this requires root privileges and is far more -opaque than the implementation in this library, and also requires that -`readlink` is inside the `root` path and is trustworthy): - -```go -package securejoin - -import ( - "os/exec" - "path/filepath" -) - -func SecureJoin(root, unsafePath string) (string, error) { - unsafePath = string(filepath.Separator) + unsafePath - cmd := exec.Command("chroot", root, - "readlink", "--canonicalize-missing", "--no-newline", unsafePath) - output, err := cmd.CombinedOutput() - if err != nil { - return "", err - } - expanded := string(output) - return filepath.Join(root, expanded), nil -} -``` - -[libpathrs]: https://github.com/openSUSE/libpathrs -[go#20126]: https://github.com/golang/go/issues/20126 - -### New API ### - -While we recommend users switch to [libpathrs][libpathrs] as soon as it has a -stable release, some methods implemented by libpathrs have been ported to this -library to ease the transition. These APIs are only supported on Linux. - -These APIs are implemented such that `filepath-securejoin` will -opportunistically use certain newer kernel APIs that make these operations far -more secure. In particular: - -* All of the lookup operations will use [`openat2`][openat2.2] on new enough - kernels (Linux 5.6 or later) to restrict lookups through magic-links and - bind-mounts (for certain operations) and to make use of `RESOLVE_IN_ROOT` to - efficiently resolve symlinks within a rootfs. - -* The APIs provide hardening against a malicious `/proc` mount to either detect - or avoid being tricked by a `/proc` that is not legitimate. This is done - using [`openat2`][openat2.2] for all users, and privileged users will also be - further protected by using [`fsopen`][fsopen.2] and [`open_tree`][open_tree.2] - (Linux 5.2 or later). - -[openat2.2]: https://www.man7.org/linux/man-pages/man2/openat2.2.html -[fsopen.2]: https://github.com/brauner/man-pages-md/blob/main/fsopen.md -[open_tree.2]: https://github.com/brauner/man-pages-md/blob/main/open_tree.md - -#### `OpenInRoot` #### - -```go -func OpenInRoot(root, unsafePath string) (*os.File, error) -func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) -func Reopen(handle *os.File, flags int) (*os.File, error) -``` - -`OpenInRoot` is a much safer version of - -```go -path, err := securejoin.SecureJoin(root, unsafePath) -file, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC) -``` - -that protects against various race attacks that could lead to serious security -issues, depending on the application. Note that the returned `*os.File` is an -`O_PATH` file descriptor, which is quite restricted. Callers will probably need -to use `Reopen` to get a more usable handle (this split is done to provide -useful features like PTY spawning and to avoid users accidentally opening bad -inodes that could cause a DoS). - -Callers need to be careful in how they use the returned `*os.File`. Usually it -is only safe to operate on the handle directly, and it is very easy to create a -security issue. [libpathrs][libpathrs] provides far more helpers to make using -these handles safer -- there is currently no plan to port them to -`filepath-securejoin`. - -`OpenatInRoot` is like `OpenInRoot` except that the root is provided using an -`*os.File`. This allows you to ensure that multiple `OpenatInRoot` (or -`MkdirAllHandle`) calls are operating on the same rootfs. - -> **NOTE**: Unlike `SecureJoin`, `OpenInRoot` will error out as soon as it hits -> a dangling symlink or non-existent path. This is in contrast to `SecureJoin` -> which treated non-existent components as though they were real directories, -> and would allow for partial resolution of dangling symlinks. These behaviours -> are at odds with how Linux treats non-existent paths and dangling symlinks, -> and so these are no longer allowed. - -#### `MkdirAll` #### - -```go -func MkdirAll(root, unsafePath string, mode int) error -func MkdirAllHandle(root *os.File, unsafePath string, mode int) (*os.File, error) -``` - -`MkdirAll` is a much safer version of - -```go -path, err := securejoin.SecureJoin(root, unsafePath) -err = os.MkdirAll(path, mode) -``` - -that protects against the same kinds of races that `OpenInRoot` protects -against. - -`MkdirAllHandle` is like `MkdirAll` except that the root is provided using an -`*os.File` (the reason for this is the same as with `OpenatInRoot`) and an -`*os.File` of the final created directory is returned (this directory is -guaranteed to be effectively identical to the directory created by -`MkdirAllHandle`, which is not possible to ensure by just using `OpenatInRoot` -after `MkdirAll`). - -> **NOTE**: Unlike `SecureJoin`, `MkdirAll` will error out as soon as it hits -> a dangling symlink or non-existent path. This is in contrast to `SecureJoin` -> which treated non-existent components as though they were real directories, -> and would allow for partial resolution of dangling symlinks. These behaviours -> are at odds with how Linux treats non-existent paths and dangling symlinks, -> and so these are no longer allowed. This means that `MkdirAll` will not -> create non-existent directories referenced by a dangling symlink. - -### License ### - -The license of this project is the same as Go, which is a BSD 3-clause license -available in the `LICENSE` file. diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/VERSION b/backend/vendor/github.com/cyphar/filepath-securejoin/VERSION deleted file mode 100644 index 42045ac..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.3.4 diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/doc.go b/backend/vendor/github.com/cyphar/filepath-securejoin/doc.go deleted file mode 100644 index 1ec7d06..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/doc.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. -// Copyright (C) 2017-2024 SUSE LLC. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package securejoin implements a set of helpers to make it easier to write Go -// code that is safe against symlink-related escape attacks. The primary idea -// is to let you resolve a path within a rootfs directory as if the rootfs was -// a chroot. -// -// securejoin has two APIs, a "legacy" API and a "modern" API. -// -// The legacy API is [SecureJoin] and [SecureJoinVFS]. These methods are -// **not** safe against race conditions where an attacker changes the -// filesystem after (or during) the [SecureJoin] operation. -// -// The new API is made up of [OpenInRoot] and [MkdirAll] (and derived -// functions). These are safe against racing attackers and have several other -// protections that are not provided by the legacy API. There are many more -// operations that most programs expect to be able to do safely, but we do not -// provide explicit support for them because we want to encourage users to -// switch to [libpathrs](https://github.com/openSUSE/libpathrs) which is a -// cross-language next-generation library that is entirely designed around -// operating on paths safely. -// -// securejoin has been used by several container runtimes (Docker, runc, -// Kubernetes, etc) for quite a few years as a de-facto standard for operating -// on container filesystem paths "safely". However, most users still use the -// legacy API which is unsafe against various attacks (there is a fairly long -// history of CVEs in dependent as a result). Users should switch to the modern -// API as soon as possible (or even better, switch to libpathrs). -// -// This project was initially intended to be included in the Go standard -// library, but [it was rejected](https://go.dev/issue/20126). There is now a -// [new Go proposal](https://go.dev/issue/67002) for a safe path resolution API -// that shares some of the goals of filepath-securejoin. However, that design -// is intended to work like `openat2(RESOLVE_BENEATH)` which does not fit the -// usecase of container runtimes and most system tools. -package securejoin diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/join.go b/backend/vendor/github.com/cyphar/filepath-securejoin/join.go deleted file mode 100644 index e0ee3f2..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/join.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. -// Copyright (C) 2017-2024 SUSE LLC. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package securejoin - -import ( - "errors" - "os" - "path/filepath" - "strings" - "syscall" -) - -const maxSymlinkLimit = 255 - -// IsNotExist tells you if err is an error that implies that either the path -// accessed does not exist (or path components don't exist). This is -// effectively a more broad version of [os.IsNotExist]. -func IsNotExist(err error) bool { - // Check that it's not actually an ENOTDIR, which in some cases is a more - // convoluted case of ENOENT (usually involving weird paths). - return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT) -} - -// SecureJoinVFS joins the two given path components (similar to [filepath.Join]) except -// that the returned path is guaranteed to be scoped inside the provided root -// path (when evaluated). Any symbolic links in the path are evaluated with the -// given root treated as the root of the filesystem, similar to a chroot. The -// filesystem state is evaluated through the given [VFS] interface (if nil, the -// standard [os].* family of functions are used). -// -// Note that the guarantees provided by this function only apply if the path -// components in the returned string are not modified (in other words are not -// replaced with symlinks on the filesystem) after this function has returned. -// Such a symlink race is necessarily out-of-scope of SecureJoinVFS. -// -// NOTE: Due to the above limitation, Linux users are strongly encouraged to -// use [OpenInRoot] instead, which does safely protect against these kinds of -// attacks. There is no way to solve this problem with SecureJoinVFS because -// the API is fundamentally wrong (you cannot return a "safe" path string and -// guarantee it won't be modified afterwards). -// -// Volume names in unsafePath are always discarded, regardless if they are -// provided via direct input or when evaluating symlinks. Therefore: -// -// "C:\Temp" + "D:\path\to\file.txt" results in "C:\Temp\path\to\file.txt" -func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { - // Use the os.* VFS implementation if none was specified. - if vfs == nil { - vfs = osVFS{} - } - - unsafePath = filepath.FromSlash(unsafePath) - var ( - currentPath string - remainingPath = unsafePath - linksWalked int - ) - for remainingPath != "" { - if v := filepath.VolumeName(remainingPath); v != "" { - remainingPath = remainingPath[len(v):] - } - - // Get the next path component. - var part string - if i := strings.IndexRune(remainingPath, filepath.Separator); i == -1 { - part, remainingPath = remainingPath, "" - } else { - part, remainingPath = remainingPath[:i], remainingPath[i+1:] - } - - // Apply the component lexically to the path we are building. - // currentPath does not contain any symlinks, and we are lexically - // dealing with a single component, so it's okay to do a filepath.Clean - // here. - nextPath := filepath.Join(string(filepath.Separator), currentPath, part) - if nextPath == string(filepath.Separator) { - currentPath = "" - continue - } - fullPath := root + string(filepath.Separator) + nextPath - - // Figure out whether the path is a symlink. - fi, err := vfs.Lstat(fullPath) - if err != nil && !IsNotExist(err) { - return "", err - } - // Treat non-existent path components the same as non-symlinks (we - // can't do any better here). - if IsNotExist(err) || fi.Mode()&os.ModeSymlink == 0 { - currentPath = nextPath - continue - } - - // It's a symlink, so get its contents and expand it by prepending it - // to the yet-unparsed path. - linksWalked++ - if linksWalked > maxSymlinkLimit { - return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP} - } - - dest, err := vfs.Readlink(fullPath) - if err != nil { - return "", err - } - remainingPath = dest + string(filepath.Separator) + remainingPath - // Absolute symlinks reset any work we've already done. - if filepath.IsAbs(dest) { - currentPath = "" - } - } - - // There should be no lexical components like ".." left in the path here, - // but for safety clean up the path before joining it to the root. - finalPath := filepath.Join(string(filepath.Separator), currentPath) - return filepath.Join(root, finalPath), nil -} - -// SecureJoin is a wrapper around [SecureJoinVFS] that just uses the [os].* library -// of functions as the [VFS]. If in doubt, use this function over [SecureJoinVFS]. -func SecureJoin(root, unsafePath string) (string, error) { - return SecureJoinVFS(root, unsafePath, nil) -} diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/lookup_linux.go b/backend/vendor/github.com/cyphar/filepath-securejoin/lookup_linux.go deleted file mode 100644 index 290befa..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/lookup_linux.go +++ /dev/null @@ -1,389 +0,0 @@ -//go:build linux - -// Copyright (C) 2024 SUSE LLC. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package securejoin - -import ( - "errors" - "fmt" - "os" - "path" - "path/filepath" - "slices" - "strings" - - "golang.org/x/sys/unix" -) - -type symlinkStackEntry struct { - // (dir, remainingPath) is what we would've returned if the link didn't - // exist. This matches what openat2(RESOLVE_IN_ROOT) would return in - // this case. - dir *os.File - remainingPath string - // linkUnwalked is the remaining path components from the original - // Readlink which we have yet to walk. When this slice is empty, we - // drop the link from the stack. - linkUnwalked []string -} - -func (se symlinkStackEntry) String() string { - return fmt.Sprintf("<%s>/%s [->%s]", se.dir.Name(), se.remainingPath, strings.Join(se.linkUnwalked, "/")) -} - -func (se symlinkStackEntry) Close() { - _ = se.dir.Close() -} - -type symlinkStack []*symlinkStackEntry - -func (s *symlinkStack) IsEmpty() bool { - return s == nil || len(*s) == 0 -} - -func (s *symlinkStack) Close() { - if s != nil { - for _, link := range *s { - link.Close() - } - // TODO: Switch to clear once we switch to Go 1.21. - *s = nil - } -} - -var ( - errEmptyStack = errors.New("[internal] stack is empty") - errBrokenSymlinkStack = errors.New("[internal error] broken symlink stack") -) - -func (s *symlinkStack) popPart(part string) error { - if s == nil || s.IsEmpty() { - // If there is nothing in the symlink stack, then the part was from the - // real path provided by the user, and this is a no-op. - return errEmptyStack - } - if part == "." { - // "." components are no-ops -- we drop them when doing SwapLink. - return nil - } - - tailEntry := (*s)[len(*s)-1] - - // Double-check that we are popping the component we expect. - if len(tailEntry.linkUnwalked) == 0 { - return fmt.Errorf("%w: trying to pop component %q of empty stack entry %s", errBrokenSymlinkStack, part, tailEntry) - } - headPart := tailEntry.linkUnwalked[0] - if headPart != part { - return fmt.Errorf("%w: trying to pop component %q but the last stack entry is %s (%q)", errBrokenSymlinkStack, part, tailEntry, headPart) - } - - // Drop the component, but keep the entry around in case we are dealing - // with a "tail-chained" symlink. - tailEntry.linkUnwalked = tailEntry.linkUnwalked[1:] - return nil -} - -func (s *symlinkStack) PopPart(part string) error { - if err := s.popPart(part); err != nil { - if errors.Is(err, errEmptyStack) { - // Skip empty stacks. - err = nil - } - return err - } - - // Clean up any of the trailing stack entries that are empty. - for lastGood := len(*s) - 1; lastGood >= 0; lastGood-- { - entry := (*s)[lastGood] - if len(entry.linkUnwalked) > 0 { - break - } - entry.Close() - (*s) = (*s)[:lastGood] - } - return nil -} - -func (s *symlinkStack) push(dir *os.File, remainingPath, linkTarget string) error { - if s == nil { - return nil - } - // Split the link target and clean up any "" parts. - linkTargetParts := slices.DeleteFunc( - strings.Split(linkTarget, "/"), - func(part string) bool { return part == "" || part == "." }) - - // Copy the directory so the caller doesn't close our copy. - dirCopy, err := dupFile(dir) - if err != nil { - return err - } - - // Add to the stack. - *s = append(*s, &symlinkStackEntry{ - dir: dirCopy, - remainingPath: remainingPath, - linkUnwalked: linkTargetParts, - }) - return nil -} - -func (s *symlinkStack) SwapLink(linkPart string, dir *os.File, remainingPath, linkTarget string) error { - // If we are currently inside a symlink resolution, remove the symlink - // component from the last symlink entry, but don't remove the entry even - // if it's empty. If we are a "tail-chained" symlink (a trailing symlink we - // hit during a symlink resolution) we need to keep the old symlink until - // we finish the resolution. - if err := s.popPart(linkPart); err != nil { - if !errors.Is(err, errEmptyStack) { - return err - } - // Push the component regardless of whether the stack was empty. - } - return s.push(dir, remainingPath, linkTarget) -} - -func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) { - if s == nil || s.IsEmpty() { - return nil, "", false - } - tailEntry := (*s)[0] - *s = (*s)[1:] - return tailEntry.dir, tailEntry.remainingPath, true -} - -// partialLookupInRoot tries to lookup as much of the request path as possible -// within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing -// component of the requested path, returning a file handle to the final -// existing component and a string containing the remaining path components. -func partialLookupInRoot(root *os.File, unsafePath string) (*os.File, string, error) { - return lookupInRoot(root, unsafePath, true) -} - -func completeLookupInRoot(root *os.File, unsafePath string) (*os.File, error) { - handle, remainingPath, err := lookupInRoot(root, unsafePath, false) - if remainingPath != "" && err == nil { - // should never happen - err = fmt.Errorf("[bug] non-empty remaining path when doing a non-partial lookup: %q", remainingPath) - } - // lookupInRoot(partial=false) will always close the handle if an error is - // returned, so no need to double-check here. - return handle, err -} - -func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) { - unsafePath = filepath.ToSlash(unsafePath) // noop - - // This is very similar to SecureJoin, except that we operate on the - // components using file descriptors. We then return the last component we - // managed open, along with the remaining path components not opened. - - // Try to use openat2 if possible. - if hasOpenat2() { - return lookupOpenat2(root, unsafePath, partial) - } - - // Get the "actual" root path from /proc/self/fd. This is necessary if the - // root is some magic-link like /proc/$pid/root, in which case we want to - // make sure when we do checkProcSelfFdPath that we are using the correct - // root path. - logicalRootPath, err := procSelfFdReadlink(root) - if err != nil { - return nil, "", fmt.Errorf("get real root path: %w", err) - } - - currentDir, err := dupFile(root) - if err != nil { - return nil, "", fmt.Errorf("clone root fd: %w", err) - } - defer func() { - // If a handle is not returned, close the internal handle. - if Handle == nil { - _ = currentDir.Close() - } - }() - - // symlinkStack is used to emulate how openat2(RESOLVE_IN_ROOT) treats - // dangling symlinks. If we hit a non-existent path while resolving a - // symlink, we need to return the (dir, remainingPath) that we had when we - // hit the symlink (treating the symlink as though it were a regular file). - // The set of (dir, remainingPath) sets is stored within the symlinkStack - // and we add and remove parts when we hit symlink and non-symlink - // components respectively. We need a stack because of recursive symlinks - // (symlinks that contain symlink components in their target). - // - // Note that the stack is ONLY used for book-keeping. All of the actual - // path walking logic is still based on currentPath/remainingPath and - // currentDir (as in SecureJoin). - var symStack *symlinkStack - if partial { - symStack = new(symlinkStack) - defer symStack.Close() - } - - var ( - linksWalked int - currentPath string - remainingPath = unsafePath - ) - for remainingPath != "" { - // Save the current remaining path so if the part is not real we can - // return the path including the component. - oldRemainingPath := remainingPath - - // Get the next path component. - var part string - if i := strings.IndexByte(remainingPath, '/'); i == -1 { - part, remainingPath = remainingPath, "" - } else { - part, remainingPath = remainingPath[:i], remainingPath[i+1:] - } - // If we hit an empty component, we need to treat it as though it is - // "." so that trailing "/" and "//" components on a non-directory - // correctly return the right error code. - if part == "" { - part = "." - } - - // Apply the component lexically to the path we are building. - // currentPath does not contain any symlinks, and we are lexically - // dealing with a single component, so it's okay to do a filepath.Clean - // here. - nextPath := path.Join("/", currentPath, part) - // If we logically hit the root, just clone the root rather than - // opening the part and doing all of the other checks. - if nextPath == "/" { - if err := symStack.PopPart(part); err != nil { - return nil, "", fmt.Errorf("walking into root with part %q failed: %w", part, err) - } - // Jump to root. - rootClone, err := dupFile(root) - if err != nil { - return nil, "", fmt.Errorf("clone root fd: %w", err) - } - _ = currentDir.Close() - currentDir = rootClone - currentPath = nextPath - continue - } - - // Try to open the next component. - nextDir, err := openatFile(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) - switch { - case err == nil: - st, err := nextDir.Stat() - if err != nil { - _ = nextDir.Close() - return nil, "", fmt.Errorf("stat component %q: %w", part, err) - } - - switch st.Mode() & os.ModeType { - case os.ModeSymlink: - // readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See - // Linux commit 65cfc6722361 ("readlinkat(), fchownat() and - // fstatat() with empty relative pathnames"). - linkDest, err := readlinkatFile(nextDir, "") - // We don't need the handle anymore. - _ = nextDir.Close() - if err != nil { - return nil, "", err - } - - linksWalked++ - if linksWalked > maxSymlinkLimit { - return nil, "", &os.PathError{Op: "securejoin.lookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP} - } - - // Swap out the symlink's component for the link entry itself. - if err := symStack.SwapLink(part, currentDir, oldRemainingPath, linkDest); err != nil { - return nil, "", fmt.Errorf("walking into symlink %q failed: push symlink: %w", part, err) - } - - // Update our logical remaining path. - remainingPath = linkDest + "/" + remainingPath - // Absolute symlinks reset any work we've already done. - if path.IsAbs(linkDest) { - // Jump to root. - rootClone, err := dupFile(root) - if err != nil { - return nil, "", fmt.Errorf("clone root fd: %w", err) - } - _ = currentDir.Close() - currentDir = rootClone - currentPath = "/" - } - - default: - // If we are dealing with a directory, simply walk into it. - _ = currentDir.Close() - currentDir = nextDir - currentPath = nextPath - - // The part was real, so drop it from the symlink stack. - if err := symStack.PopPart(part); err != nil { - return nil, "", fmt.Errorf("walking into directory %q failed: %w", part, err) - } - - // If we are operating on a .., make sure we haven't escaped. - // We only have to check for ".." here because walking down - // into a regular component component cannot cause you to - // escape. This mirrors the logic in RESOLVE_IN_ROOT, except we - // have to check every ".." rather than only checking after a - // rename or mount on the system. - if part == ".." { - // Make sure the root hasn't moved. - if err := checkProcSelfFdPath(logicalRootPath, root); err != nil { - return nil, "", fmt.Errorf("root path moved during lookup: %w", err) - } - // Make sure the path is what we expect. - fullPath := logicalRootPath + nextPath - if err := checkProcSelfFdPath(fullPath, currentDir); err != nil { - return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err) - } - } - } - - default: - if !partial { - return nil, "", err - } - // If there are any remaining components in the symlink stack, we - // are still within a symlink resolution and thus we hit a dangling - // symlink. So pretend that the first symlink in the stack we hit - // was an ENOENT (to match openat2). - if oldDir, remainingPath, ok := symStack.PopTopSymlink(); ok { - _ = currentDir.Close() - return oldDir, remainingPath, err - } - // We have hit a final component that doesn't exist, so we have our - // partial open result. Note that we have to use the OLD remaining - // path, since the lookup failed. - return currentDir, oldRemainingPath, err - } - } - - // If the unsafePath had a trailing slash, we need to make sure we try to - // do a relative "." open so that we will correctly return an error when - // the final component is a non-directory (to match openat2). In the - // context of openat2, a trailing slash and a trailing "/." are completely - // equivalent. - if strings.HasSuffix(unsafePath, "/") { - nextDir, err := openatFile(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) - if err != nil { - if !partial { - _ = currentDir.Close() - currentDir = nil - } - return currentDir, "", err - } - _ = currentDir.Close() - currentDir = nextDir - } - - // All of the components existed! - return currentDir, "", nil -} diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/mkdir_linux.go b/backend/vendor/github.com/cyphar/filepath-securejoin/mkdir_linux.go deleted file mode 100644 index b5f6745..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/mkdir_linux.go +++ /dev/null @@ -1,207 +0,0 @@ -//go:build linux - -// Copyright (C) 2024 SUSE LLC. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package securejoin - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "slices" - "strings" - - "golang.org/x/sys/unix" -) - -var ( - errInvalidMode = errors.New("invalid permission mode") - errPossibleAttack = errors.New("possible attack detected") -) - -// MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use -// in two respects: -// -// - The caller provides the root directory as an *[os.File] (preferably O_PATH) -// handle. This means that the caller can be sure which root directory is -// being used. Note that this can be emulated by using /proc/self/fd/... as -// the root path with [os.MkdirAll]. -// -// - Once all of the directories have been created, an *[os.File] O_PATH handle -// to the directory at unsafePath is returned to the caller. This is done in -// an effectively-race-free way (an attacker would only be able to swap the -// final directory component), which is not possible to emulate with -// [MkdirAll]. -// -// In addition, the returned handle is obtained far more efficiently than doing -// a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after -// doing [MkdirAll]. If you intend to open the directory after creating it, you -// should use MkdirAllHandle. -func MkdirAllHandle(root *os.File, unsafePath string, mode int) (_ *os.File, Err error) { - // Make sure there are no os.FileMode bits set. - if mode&^0o7777 != 0 { - return nil, fmt.Errorf("%w for mkdir 0o%.3o", errInvalidMode, mode) - } - // On Linux, mkdirat(2) (and os.Mkdir) silently ignore the suid and sgid - // bits. We could also silently ignore them but since we have very few - // users it seems more prudent to return an error so users notice that - // these bits will not be set. - if mode&^0o1777 != 0 { - return nil, fmt.Errorf("%w for mkdir 0o%.3o: suid and sgid are ignored by mkdir", errInvalidMode, mode) - } - - // Try to open as much of the path as possible. - currentDir, remainingPath, err := partialLookupInRoot(root, unsafePath) - defer func() { - if Err != nil { - _ = currentDir.Close() - } - }() - if err != nil && !errors.Is(err, unix.ENOENT) { - return nil, fmt.Errorf("find existing subpath of %q: %w", unsafePath, err) - } - - // If there is an attacker deleting directories as we walk into them, - // detect this proactively. Note this is guaranteed to detect if the - // attacker deleted any part of the tree up to currentDir. - // - // Once we walk into a dead directory, partialLookupInRoot would not be - // able to walk further down the tree (directories must be empty before - // they are deleted), and if the attacker has removed the entire tree we - // can be sure that anything that was originally inside a dead directory - // must also be deleted and thus is a dead directory in its own right. - // - // This is mostly a quality-of-life check, because mkdir will simply fail - // later if the attacker deletes the tree after this check. - if err := isDeadInode(currentDir); err != nil { - return nil, fmt.Errorf("finding existing subpath of %q: %w", unsafePath, err) - } - - // Re-open the path to match the O_DIRECTORY reopen loop later (so that we - // always return a non-O_PATH handle). We also check that we actually got a - // directory. - if reopenDir, err := Reopen(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) { - return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR) - } else if err != nil { - return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err) - } else { - _ = currentDir.Close() - currentDir = reopenDir - } - - remainingParts := strings.Split(remainingPath, string(filepath.Separator)) - if slices.Contains(remainingParts, "..") { - // The path contained ".." components after the end of the "real" - // components. We could try to safely resolve ".." here but that would - // add a bunch of extra logic for something that it's not clear even - // needs to be supported. So just return an error. - // - // If we do filepath.Clean(remainingPath) then we end up with the - // problem that ".." can erase a trailing dangling symlink and produce - // a path that doesn't quite match what the user asked for. - return nil, fmt.Errorf("%w: yet-to-be-created path %q contains '..' components", unix.ENOENT, remainingPath) - } - - // Make sure the mode doesn't have any type bits. - mode &^= unix.S_IFMT - - // Create the remaining components. - for _, part := range remainingParts { - switch part { - case "", ".": - // Skip over no-op paths. - continue - } - - // NOTE: mkdir(2) will not follow trailing symlinks, so we can safely - // create the final component without worrying about symlink-exchange - // attacks. - if err := unix.Mkdirat(int(currentDir.Fd()), part, uint32(mode)); err != nil { - err = &os.PathError{Op: "mkdirat", Path: currentDir.Name() + "/" + part, Err: err} - // Make the error a bit nicer if the directory is dead. - if err2 := isDeadInode(currentDir); err2 != nil { - err = fmt.Errorf("%w (%w)", err, err2) - } - return nil, err - } - - // Get a handle to the next component. O_DIRECTORY means we don't need - // to use O_PATH. - var nextDir *os.File - if hasOpenat2() { - nextDir, err = openat2File(currentDir, part, &unix.OpenHow{ - Flags: unix.O_NOFOLLOW | unix.O_DIRECTORY | unix.O_CLOEXEC, - Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV, - }) - } else { - nextDir, err = openatFile(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) - } - if err != nil { - return nil, err - } - _ = currentDir.Close() - currentDir = nextDir - - // It's possible that the directory we just opened was swapped by an - // attacker. Unfortunately there isn't much we can do to protect - // against this, and MkdirAll's behaviour is that we will reuse - // existing directories anyway so the need to protect against this is - // incredibly limited (and arguably doesn't even deserve mention here). - // - // Ideally we might want to check that the owner and mode match what we - // would've created -- unfortunately, it is non-trivial to verify that - // the owner and mode of the created directory match. While plain Unix - // DAC rules seem simple enough to emulate, there are a bunch of other - // factors that can change the mode or owner of created directories - // (default POSIX ACLs, mount options like uid=1,gid=2,umask=0 on - // filesystems like vfat, etc etc). We used to try to verify this but - // it just lead to a series of spurious errors. - // - // We could also check that the directory is non-empty, but - // unfortunately some pseduofilesystems (like cgroupfs) create - // non-empty directories, which would result in different spurious - // errors. - } - return currentDir, nil -} - -// MkdirAll is a race-safe alternative to the [os.MkdirAll] function, -// where the new directory is guaranteed to be within the root directory (if an -// attacker can move directories from inside the root to outside the root, the -// created directory tree might be outside of the root but the key constraint -// is that at no point will we walk outside of the directory tree we are -// creating). -// -// Effectively, MkdirAll(root, unsafePath, mode) is equivalent to -// -// path, _ := securejoin.SecureJoin(root, unsafePath) -// err := os.MkdirAll(path, mode) -// -// But is much safer. The above implementation is unsafe because if an attacker -// can modify the filesystem tree between [SecureJoin] and [os.MkdirAll], it is -// possible for MkdirAll to resolve unsafe symlink components and create -// directories outside of the root. -// -// If you plan to open the directory after you have created it or want to use -// an open directory handle as the root, you should use [MkdirAllHandle] instead. -// This function is a wrapper around [MkdirAllHandle]. -// -// NOTE: The mode argument must be set the unix mode bits (unix.S_I...), not -// the Go generic mode bits ([os.FileMode]...). -func MkdirAll(root, unsafePath string, mode int) error { - rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) - if err != nil { - return err - } - defer rootDir.Close() - - f, err := MkdirAllHandle(rootDir, unsafePath, mode) - if err != nil { - return err - } - _ = f.Close() - return nil -} diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/open_linux.go b/backend/vendor/github.com/cyphar/filepath-securejoin/open_linux.go deleted file mode 100644 index 230be73..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/open_linux.go +++ /dev/null @@ -1,103 +0,0 @@ -//go:build linux - -// Copyright (C) 2024 SUSE LLC. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package securejoin - -import ( - "fmt" - "os" - "strconv" - - "golang.org/x/sys/unix" -) - -// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided -// using an *[os.File] handle, to ensure that the correct root directory is used. -func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) { - handle, err := completeLookupInRoot(root, unsafePath) - if err != nil { - return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err} - } - return handle, nil -} - -// OpenInRoot safely opens the provided unsafePath within the root. -// Effectively, OpenInRoot(root, unsafePath) is equivalent to -// -// path, _ := securejoin.SecureJoin(root, unsafePath) -// handle, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC) -// -// But is much safer. The above implementation is unsafe because if an attacker -// can modify the filesystem tree between [SecureJoin] and [os.OpenFile], it is -// possible for the returned file to be outside of the root. -// -// Note that the returned handle is an O_PATH handle, meaning that only a very -// limited set of operations will work on the handle. This is done to avoid -// accidentally opening an untrusted file that could cause issues (such as a -// disconnected TTY that could cause a DoS, or some other issue). In order to -// use the returned handle, you can "upgrade" it to a proper handle using -// [Reopen]. -func OpenInRoot(root, unsafePath string) (*os.File, error) { - rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) - if err != nil { - return nil, err - } - defer rootDir.Close() - return OpenatInRoot(rootDir, unsafePath) -} - -// Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd. -// Reopen(file, flags) is effectively equivalent to -// -// fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd()) -// os.OpenFile(fdPath, flags|unix.O_CLOEXEC) -// -// But with some extra hardenings to ensure that we are not tricked by a -// maliciously-configured /proc mount. While this attack scenario is not -// common, in container runtimes it is possible for higher-level runtimes to be -// tricked into configuring an unsafe /proc that can be used to attack file -// operations. See [CVE-2019-19921] for more details. -// -// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw -func Reopen(handle *os.File, flags int) (*os.File, error) { - procRoot, err := getProcRoot() - if err != nil { - return nil, err - } - - // We can't operate on /proc/thread-self/fd/$n directly when doing a - // re-open, so we need to open /proc/thread-self/fd and then open a single - // final component. - procFdDir, closer, err := procThreadSelf(procRoot, "fd/") - if err != nil { - return nil, fmt.Errorf("get safe /proc/thread-self/fd handle: %w", err) - } - defer procFdDir.Close() - defer closer() - - // Try to detect if there is a mount on top of the magic-link we are about - // to open. If we are using unsafeHostProcRoot(), this could change after - // we check it (and there's nothing we can do about that) but for - // privateProcRoot() this should be guaranteed to be safe (at least since - // Linux 5.12[1], when anonymous mount namespaces were completely isolated - // from external mounts including mount propagation events). - // - // [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts - // onto targets that reside on shared mounts"). - fdStr := strconv.Itoa(int(handle.Fd())) - if err := checkSymlinkOvermount(procRoot, procFdDir, fdStr); err != nil { - return nil, fmt.Errorf("check safety of /proc/thread-self/fd/%s magiclink: %w", fdStr, err) - } - - flags |= unix.O_CLOEXEC - // Rather than just wrapping openatFile, open-code it so we can copy - // handle.Name(). - reopenFd, err := unix.Openat(int(procFdDir.Fd()), fdStr, flags, 0) - if err != nil { - return nil, fmt.Errorf("reopen fd %d: %w", handle.Fd(), err) - } - return os.NewFile(uintptr(reopenFd), handle.Name()), nil -} diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/openat2_linux.go b/backend/vendor/github.com/cyphar/filepath-securejoin/openat2_linux.go deleted file mode 100644 index ae3b381..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/openat2_linux.go +++ /dev/null @@ -1,128 +0,0 @@ -//go:build linux - -// Copyright (C) 2024 SUSE LLC. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package securejoin - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "sync" - - "golang.org/x/sys/unix" -) - -var hasOpenat2 = sync.OnceValue(func() bool { - fd, err := unix.Openat2(unix.AT_FDCWD, ".", &unix.OpenHow{ - Flags: unix.O_PATH | unix.O_CLOEXEC, - Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_IN_ROOT, - }) - if err != nil { - return false - } - _ = unix.Close(fd) - return true -}) - -func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool { - // RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve - // ".." while a mount or rename occurs anywhere on the system. This could - // happen spuriously, or as the result of an attacker trying to mess with - // us during lookup. - // - // In addition, scoped lookups have a "safety check" at the end of - // complete_walk which will return -EXDEV if the final path is not in the - // root. - return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 && - (errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV)) -} - -const scopedLookupMaxRetries = 10 - -func openat2File(dir *os.File, path string, how *unix.OpenHow) (*os.File, error) { - fullPath := dir.Name() + "/" + path - // Make sure we always set O_CLOEXEC. - how.Flags |= unix.O_CLOEXEC - var tries int - for tries < scopedLookupMaxRetries { - fd, err := unix.Openat2(int(dir.Fd()), path, how) - if err != nil { - if scopedLookupShouldRetry(how, err) { - // We retry a couple of times to avoid the spurious errors, and - // if we are being attacked then returning -EAGAIN is the best - // we can do. - tries++ - continue - } - return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err} - } - // If we are using RESOLVE_IN_ROOT, the name we generated may be wrong. - // NOTE: The procRoot code MUST NOT use RESOLVE_IN_ROOT, otherwise - // you'll get infinite recursion here. - if how.Resolve&unix.RESOLVE_IN_ROOT == unix.RESOLVE_IN_ROOT { - if actualPath, err := rawProcSelfFdReadlink(fd); err == nil { - fullPath = actualPath - } - } - return os.NewFile(uintptr(fd), fullPath), nil - } - return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: errPossibleAttack} -} - -func lookupOpenat2(root *os.File, unsafePath string, partial bool) (*os.File, string, error) { - if !partial { - file, err := openat2File(root, unsafePath, &unix.OpenHow{ - Flags: unix.O_PATH | unix.O_CLOEXEC, - Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS, - }) - return file, "", err - } - return partialLookupOpenat2(root, unsafePath) -} - -// partialLookupOpenat2 is an alternative implementation of -// partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a -// handle to the deepest existing child of the requested path within the root. -func partialLookupOpenat2(root *os.File, unsafePath string) (*os.File, string, error) { - // TODO: Implement this as a git-bisect-like binary search. - - unsafePath = filepath.ToSlash(unsafePath) // noop - endIdx := len(unsafePath) - var lastError error - for endIdx > 0 { - subpath := unsafePath[:endIdx] - - handle, err := openat2File(root, subpath, &unix.OpenHow{ - Flags: unix.O_PATH | unix.O_CLOEXEC, - Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS, - }) - if err == nil { - // Jump over the slash if we have a non-"" remainingPath. - if endIdx < len(unsafePath) { - endIdx += 1 - } - // We found a subpath! - return handle, unsafePath[endIdx:], lastError - } - if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) { - // That path doesn't exist, let's try the next directory up. - endIdx = strings.LastIndexByte(subpath, '/') - lastError = err - continue - } - return nil, "", fmt.Errorf("open subpath: %w", err) - } - // If we couldn't open anything, the whole subpath is missing. Return a - // copy of the root fd so that the caller doesn't close this one by - // accident. - rootClone, err := dupFile(root) - if err != nil { - return nil, "", err - } - return rootClone, unsafePath, lastError -} diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/openat_linux.go b/backend/vendor/github.com/cyphar/filepath-securejoin/openat_linux.go deleted file mode 100644 index 949fb5f..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/openat_linux.go +++ /dev/null @@ -1,59 +0,0 @@ -//go:build linux - -// Copyright (C) 2024 SUSE LLC. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package securejoin - -import ( - "os" - "path/filepath" - - "golang.org/x/sys/unix" -) - -func dupFile(f *os.File) (*os.File, error) { - fd, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0) - if err != nil { - return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err) - } - return os.NewFile(uintptr(fd), f.Name()), nil -} - -func openatFile(dir *os.File, path string, flags int, mode int) (*os.File, error) { - // Make sure we always set O_CLOEXEC. - flags |= unix.O_CLOEXEC - fd, err := unix.Openat(int(dir.Fd()), path, flags, uint32(mode)) - if err != nil { - return nil, &os.PathError{Op: "openat", Path: dir.Name() + "/" + path, Err: err} - } - // All of the paths we use with openatFile(2) are guaranteed to be - // lexically safe, so we can use path.Join here. - fullPath := filepath.Join(dir.Name(), path) - return os.NewFile(uintptr(fd), fullPath), nil -} - -func fstatatFile(dir *os.File, path string, flags int) (unix.Stat_t, error) { - var stat unix.Stat_t - if err := unix.Fstatat(int(dir.Fd()), path, &stat, flags); err != nil { - return stat, &os.PathError{Op: "fstatat", Path: dir.Name() + "/" + path, Err: err} - } - return stat, nil -} - -func readlinkatFile(dir *os.File, path string) (string, error) { - size := 4096 - for { - linkBuf := make([]byte, size) - n, err := unix.Readlinkat(int(dir.Fd()), path, linkBuf) - if err != nil { - return "", &os.PathError{Op: "readlinkat", Path: dir.Name() + "/" + path, Err: err} - } - if n != size { - return string(linkBuf[:n]), nil - } - // Possible truncation, resize the buffer. - size *= 2 - } -} diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/procfs_linux.go b/backend/vendor/github.com/cyphar/filepath-securejoin/procfs_linux.go deleted file mode 100644 index 8cc827d..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/procfs_linux.go +++ /dev/null @@ -1,440 +0,0 @@ -//go:build linux - -// Copyright (C) 2024 SUSE LLC. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package securejoin - -import ( - "errors" - "fmt" - "os" - "runtime" - "strconv" - "sync" - - "golang.org/x/sys/unix" -) - -func fstat(f *os.File) (unix.Stat_t, error) { - var stat unix.Stat_t - if err := unix.Fstat(int(f.Fd()), &stat); err != nil { - return stat, &os.PathError{Op: "fstat", Path: f.Name(), Err: err} - } - return stat, nil -} - -func fstatfs(f *os.File) (unix.Statfs_t, error) { - var statfs unix.Statfs_t - if err := unix.Fstatfs(int(f.Fd()), &statfs); err != nil { - return statfs, &os.PathError{Op: "fstatfs", Path: f.Name(), Err: err} - } - return statfs, nil -} - -// The kernel guarantees that the root inode of a procfs mount has an -// f_type of PROC_SUPER_MAGIC and st_ino of PROC_ROOT_INO. -const ( - procSuperMagic = 0x9fa0 // PROC_SUPER_MAGIC - procRootIno = 1 // PROC_ROOT_INO -) - -func verifyProcRoot(procRoot *os.File) error { - if statfs, err := fstatfs(procRoot); err != nil { - return err - } else if statfs.Type != procSuperMagic { - return fmt.Errorf("%w: incorrect procfs root filesystem type 0x%x", errUnsafeProcfs, statfs.Type) - } - if stat, err := fstat(procRoot); err != nil { - return err - } else if stat.Ino != procRootIno { - return fmt.Errorf("%w: incorrect procfs root inode number %d", errUnsafeProcfs, stat.Ino) - } - return nil -} - -var hasNewMountApi = sync.OnceValue(func() bool { - // All of the pieces of the new mount API we use (fsopen, fsconfig, - // fsmount, open_tree) were added together in Linux 5.1[1,2], so we can - // just check for one of the syscalls and the others should also be - // available. - // - // Just try to use open_tree(2) to open a file without OPEN_TREE_CLONE. - // This is equivalent to openat(2), but tells us if open_tree is - // available (and thus all of the other basic new mount API syscalls). - // open_tree(2) is most light-weight syscall to test here. - // - // [1]: merge commit 400913252d09 - // [2]: - fd, err := unix.OpenTree(-int(unix.EBADF), "/", unix.OPEN_TREE_CLOEXEC) - if err != nil { - return false - } - _ = unix.Close(fd) - return true -}) - -func fsopen(fsName string, flags int) (*os.File, error) { - // Make sure we always set O_CLOEXEC. - flags |= unix.FSOPEN_CLOEXEC - fd, err := unix.Fsopen(fsName, flags) - if err != nil { - return nil, os.NewSyscallError("fsopen "+fsName, err) - } - return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil -} - -func fsmount(ctx *os.File, flags, mountAttrs int) (*os.File, error) { - // Make sure we always set O_CLOEXEC. - flags |= unix.FSMOUNT_CLOEXEC - fd, err := unix.Fsmount(int(ctx.Fd()), flags, mountAttrs) - if err != nil { - return nil, os.NewSyscallError("fsmount "+ctx.Name(), err) - } - return os.NewFile(uintptr(fd), "fsmount:"+ctx.Name()), nil -} - -func newPrivateProcMount() (*os.File, error) { - procfsCtx, err := fsopen("proc", unix.FSOPEN_CLOEXEC) - if err != nil { - return nil, err - } - defer procfsCtx.Close() - - // Try to configure hidepid=ptraceable,subset=pid if possible, but ignore errors. - _ = unix.FsconfigSetString(int(procfsCtx.Fd()), "hidepid", "ptraceable") - _ = unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid") - - // Get an actual handle. - if err := unix.FsconfigCreate(int(procfsCtx.Fd())); err != nil { - return nil, os.NewSyscallError("fsconfig create procfs", err) - } - return fsmount(procfsCtx, unix.FSMOUNT_CLOEXEC, unix.MS_RDONLY|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_NOSUID) -} - -func openTree(dir *os.File, path string, flags uint) (*os.File, error) { - dirFd := -int(unix.EBADF) - dirName := "." - if dir != nil { - dirFd = int(dir.Fd()) - dirName = dir.Name() - } - // Make sure we always set O_CLOEXEC. - flags |= unix.OPEN_TREE_CLOEXEC - fd, err := unix.OpenTree(dirFd, path, flags) - if err != nil { - return nil, &os.PathError{Op: "open_tree", Path: path, Err: err} - } - return os.NewFile(uintptr(fd), dirName+"/"+path), nil -} - -func clonePrivateProcMount() (_ *os.File, Err error) { - // Try to make a clone without using AT_RECURSIVE if we can. If this works, - // we can be sure there are no over-mounts and so if the root is valid then - // we're golden. Otherwise, we have to deal with over-mounts. - procfsHandle, err := openTree(nil, "/proc", unix.OPEN_TREE_CLONE) - if err != nil || hookForcePrivateProcRootOpenTreeAtRecursive(procfsHandle) { - procfsHandle, err = openTree(nil, "/proc", unix.OPEN_TREE_CLONE|unix.AT_RECURSIVE) - } - if err != nil { - return nil, fmt.Errorf("creating a detached procfs clone: %w", err) - } - defer func() { - if Err != nil { - _ = procfsHandle.Close() - } - }() - if err := verifyProcRoot(procfsHandle); err != nil { - return nil, err - } - return procfsHandle, nil -} - -func privateProcRoot() (*os.File, error) { - if !hasNewMountApi() || hookForceGetProcRootUnsafe() { - return nil, fmt.Errorf("new mount api: %w", unix.ENOTSUP) - } - // Try to create a new procfs mount from scratch if we can. This ensures we - // can get a procfs mount even if /proc is fake (for whatever reason). - procRoot, err := newPrivateProcMount() - if err != nil || hookForcePrivateProcRootOpenTree(procRoot) { - // Try to clone /proc then... - procRoot, err = clonePrivateProcMount() - } - return procRoot, err -} - -func unsafeHostProcRoot() (_ *os.File, Err error) { - procRoot, err := os.OpenFile("/proc", unix.O_PATH|unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) - if err != nil { - return nil, err - } - defer func() { - if Err != nil { - _ = procRoot.Close() - } - }() - if err := verifyProcRoot(procRoot); err != nil { - return nil, err - } - return procRoot, nil -} - -func doGetProcRoot() (*os.File, error) { - procRoot, err := privateProcRoot() - if err != nil { - // Fall back to using a /proc handle if making a private mount failed. - // If we have openat2, at least we can avoid some kinds of over-mount - // attacks, but without openat2 there's not much we can do. - procRoot, err = unsafeHostProcRoot() - } - return procRoot, err -} - -var getProcRoot = sync.OnceValues(func() (*os.File, error) { - return doGetProcRoot() -}) - -var hasProcThreadSelf = sync.OnceValue(func() bool { - return unix.Access("/proc/thread-self/", unix.F_OK) == nil -}) - -var errUnsafeProcfs = errors.New("unsafe procfs detected") - -type procThreadSelfCloser func() - -// procThreadSelf returns a handle to /proc/thread-self/ (or an -// equivalent handle on older kernels where /proc/thread-self doesn't exist). -// Once finished with the handle, you must call the returned closer function -// (runtime.UnlockOSThread). You must not pass the returned *os.File to other -// Go threads or use the handle after calling the closer. -// -// This is similar to ProcThreadSelf from runc, but with extra hardening -// applied and using *os.File. -func procThreadSelf(procRoot *os.File, subpath string) (_ *os.File, _ procThreadSelfCloser, Err error) { - // We need to lock our thread until the caller is done with the handle - // because between getting the handle and using it we could get interrupted - // by the Go runtime and hit the case where the underlying thread is - // swapped out and the original thread is killed, resulting in - // pull-your-hair-out-hard-to-debug issues in the caller. - runtime.LockOSThread() - defer func() { - if Err != nil { - runtime.UnlockOSThread() - } - }() - - // Figure out what prefix we want to use. - threadSelf := "thread-self/" - if !hasProcThreadSelf() || hookForceProcSelfTask() { - /// Pre-3.17 kernels don't have /proc/thread-self, so do it manually. - threadSelf = "self/task/" + strconv.Itoa(unix.Gettid()) + "/" - if _, err := fstatatFile(procRoot, threadSelf, unix.AT_SYMLINK_NOFOLLOW); err != nil || hookForceProcSelf() { - // In this case, we running in a pid namespace that doesn't match - // the /proc mount we have. This can happen inside runc. - // - // Unfortunately, there is no nice way to get the correct TID to - // use here because of the age of the kernel, so we have to just - // use /proc/self and hope that it works. - threadSelf = "self/" - } - } - - // Grab the handle. - var ( - handle *os.File - err error - ) - if hasOpenat2() { - // We prefer being able to use RESOLVE_NO_XDEV if we can, to be - // absolutely sure we are operating on a clean /proc handle that - // doesn't have any cheeky overmounts that could trick us (including - // symlink mounts on top of /proc/thread-self). RESOLVE_BENEATH isn't - // strictly needed, but just use it since we have it. - // - // NOTE: /proc/self is technically a magic-link (the contents of the - // symlink are generated dynamically), but it doesn't use - // nd_jump_link() so RESOLVE_NO_MAGICLINKS allows it. - // - // NOTE: We MUST NOT use RESOLVE_IN_ROOT here, as openat2File uses - // procSelfFdReadlink to clean up the returned f.Name() if we use - // RESOLVE_IN_ROOT (which would lead to an infinite recursion). - handle, err = openat2File(procRoot, threadSelf+subpath, &unix.OpenHow{ - Flags: unix.O_PATH | unix.O_NOFOLLOW | unix.O_CLOEXEC, - Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_MAGICLINKS, - }) - if err != nil { - return nil, nil, fmt.Errorf("%w: %w", errUnsafeProcfs, err) - } - } else { - handle, err = openatFile(procRoot, threadSelf+subpath, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) - if err != nil { - return nil, nil, fmt.Errorf("%w: %w", errUnsafeProcfs, err) - } - defer func() { - if Err != nil { - _ = handle.Close() - } - }() - // We can't detect bind-mounts of different parts of procfs on top of - // /proc (a-la RESOLVE_NO_XDEV), but we can at least be sure that we - // aren't on the wrong filesystem here. - if statfs, err := fstatfs(handle); err != nil { - return nil, nil, err - } else if statfs.Type != procSuperMagic { - return nil, nil, fmt.Errorf("%w: incorrect /proc/self/fd filesystem type 0x%x", errUnsafeProcfs, statfs.Type) - } - } - return handle, runtime.UnlockOSThread, nil -} - -var hasStatxMountId = sync.OnceValue(func() bool { - var ( - stx unix.Statx_t - // We don't care which mount ID we get. The kernel will give us the - // unique one if it is supported. - wantStxMask uint32 = unix.STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID - ) - err := unix.Statx(-int(unix.EBADF), "/", 0, int(wantStxMask), &stx) - return err == nil && stx.Mask&wantStxMask != 0 -}) - -func getMountId(dir *os.File, path string) (uint64, error) { - // If we don't have statx(STATX_MNT_ID*) support, we can't do anything. - if !hasStatxMountId() { - return 0, nil - } - - var ( - stx unix.Statx_t - // We don't care which mount ID we get. The kernel will give us the - // unique one if it is supported. - wantStxMask uint32 = unix.STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID - ) - - err := unix.Statx(int(dir.Fd()), path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, int(wantStxMask), &stx) - if stx.Mask&wantStxMask == 0 { - // It's not a kernel limitation, for some reason we couldn't get a - // mount ID. Assume it's some kind of attack. - err = fmt.Errorf("%w: could not get mount id", errUnsafeProcfs) - } - if err != nil { - return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: dir.Name() + "/" + path, Err: err} - } - return stx.Mnt_id, nil -} - -func checkSymlinkOvermount(procRoot *os.File, dir *os.File, path string) error { - // Get the mntId of our procfs handle. - expectedMountId, err := getMountId(procRoot, "") - if err != nil { - return err - } - // Get the mntId of the target magic-link. - gotMountId, err := getMountId(dir, path) - if err != nil { - return err - } - // As long as the directory mount is alive, even with wrapping mount IDs, - // we would expect to see a different mount ID here. (Of course, if we're - // using unsafeHostProcRoot() then an attaker could change this after we - // did this check.) - if expectedMountId != gotMountId { - return fmt.Errorf("%w: symlink %s/%s has an overmount obscuring the real link (mount ids do not match %d != %d)", errUnsafeProcfs, dir.Name(), path, expectedMountId, gotMountId) - } - return nil -} - -func doRawProcSelfFdReadlink(procRoot *os.File, fd int) (string, error) { - fdPath := fmt.Sprintf("fd/%d", fd) - procFdLink, closer, err := procThreadSelf(procRoot, fdPath) - if err != nil { - return "", fmt.Errorf("get safe /proc/thread-self/%s handle: %w", fdPath, err) - } - defer procFdLink.Close() - defer closer() - - // Try to detect if there is a mount on top of the magic-link. Since we use the handle directly - // provide to the closure. If the closure uses the handle directly, this - // should be safe in general (a mount on top of the path afterwards would - // not affect the handle itself) and will definitely be safe if we are - // using privateProcRoot() (at least since Linux 5.12[1], when anonymous - // mount namespaces were completely isolated from external mounts including - // mount propagation events). - // - // [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts - // onto targets that reside on shared mounts"). - if err := checkSymlinkOvermount(procRoot, procFdLink, ""); err != nil { - return "", fmt.Errorf("check safety of /proc/thread-self/fd/%d magiclink: %w", fd, err) - } - - // readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See Linux commit - // 65cfc6722361 ("readlinkat(), fchownat() and fstatat() with empty - // relative pathnames"). - return readlinkatFile(procFdLink, "") -} - -func rawProcSelfFdReadlink(fd int) (string, error) { - procRoot, err := getProcRoot() - if err != nil { - return "", err - } - return doRawProcSelfFdReadlink(procRoot, fd) -} - -func procSelfFdReadlink(f *os.File) (string, error) { - return rawProcSelfFdReadlink(int(f.Fd())) -} - -var ( - errPossibleBreakout = errors.New("possible breakout detected") - errInvalidDirectory = errors.New("wandered into deleted directory") - errDeletedInode = errors.New("cannot verify path of deleted inode") -) - -func isDeadInode(file *os.File) error { - // If the nlink of a file drops to 0, there is an attacker deleting - // directories during our walk, which could result in weird /proc values. - // It's better to error out in this case. - stat, err := fstat(file) - if err != nil { - return fmt.Errorf("check for dead inode: %w", err) - } - if stat.Nlink == 0 { - err := errDeletedInode - if stat.Mode&unix.S_IFMT == unix.S_IFDIR { - err = errInvalidDirectory - } - return fmt.Errorf("%w %q", err, file.Name()) - } - return nil -} - -func checkProcSelfFdPath(path string, file *os.File) error { - if err := isDeadInode(file); err != nil { - return err - } - actualPath, err := procSelfFdReadlink(file) - if err != nil { - return fmt.Errorf("get path of handle: %w", err) - } - if actualPath != path { - return fmt.Errorf("%w: handle path %q doesn't match expected path %q", errPossibleBreakout, actualPath, path) - } - return nil -} - -// Test hooks used in the procfs tests to verify that the fallback logic works. -// See testing_mocks_linux_test.go and procfs_linux_test.go for more details. -var ( - hookForcePrivateProcRootOpenTree = hookDummyFile - hookForcePrivateProcRootOpenTreeAtRecursive = hookDummyFile - hookForceGetProcRootUnsafe = hookDummy - - hookForceProcSelfTask = hookDummy - hookForceProcSelf = hookDummy -) - -func hookDummy() bool { return false } -func hookDummyFile(_ *os.File) bool { return false } diff --git a/backend/vendor/github.com/cyphar/filepath-securejoin/vfs.go b/backend/vendor/github.com/cyphar/filepath-securejoin/vfs.go deleted file mode 100644 index 36373f8..0000000 --- a/backend/vendor/github.com/cyphar/filepath-securejoin/vfs.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2017-2024 SUSE LLC. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package securejoin - -import "os" - -// In future this should be moved into a separate package, because now there -// are several projects (umoci and go-mtree) that are using this sort of -// interface. - -// VFS is the minimal interface necessary to use [SecureJoinVFS]. A nil VFS is -// equivalent to using the standard [os].* family of functions. This is mainly -// used for the purposes of mock testing, but also can be used to otherwise use -// [SecureJoinVFS] with VFS-like system. -type VFS interface { - // Lstat returns an [os.FileInfo] describing the named file. If the - // file is a symbolic link, the returned [os.FileInfo] describes the - // symbolic link. Lstat makes no attempt to follow the link. - // The semantics are identical to [os.Lstat]. - Lstat(name string) (os.FileInfo, error) - - // Readlink returns the destination of the named symbolic link. - // The semantics are identical to [os.Readlink]. - Readlink(name string) (string, error) -} - -// osVFS is the "nil" VFS, in that it just passes everything through to the os -// module. -type osVFS struct{} - -func (o osVFS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) } - -func (o osVFS) Readlink(name string) (string, error) { return os.Readlink(name) } diff --git a/backend/vendor/modules.txt b/backend/vendor/modules.txt index 3281b2e..55d7fd3 100644 --- a/backend/vendor/modules.txt +++ b/backend/vendor/modules.txt @@ -109,9 +109,6 @@ github.com/cloudwego/base64x ## explicit; go 1.16 github.com/cloudwego/iasm/expr github.com/cloudwego/iasm/x86_64 -# github.com/cyphar/filepath-securejoin v0.3.4 -## explicit; go 1.21 -github.com/cyphar/filepath-securejoin # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew