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` ##
-
-[](https://pkg.go.dev/github.com/cyphar/filepath-securejoin)
-[](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