server(filesystem): rebuild everything imaginable

This wonderfully large commit replaces basically everything under the
`server/filesystem` package, re-implementing essentially everything.

This is related to
https://github.com/pterodactyl/wings/security/advisories/GHSA-494h-9924-xww9

If any vulnerabilities related to symlinks persist after this commit, I
will be very upset.

Signed-off-by: Matthew Penner <me@matthewp.io>
This commit is contained in:
Matthew Penner
2024-03-12 21:44:55 -06:00
parent 27f3e76c77
commit d1c0ca5260
51 changed files with 3694 additions and 1225 deletions

View File

@@ -167,13 +167,8 @@ func (dl *Download) Execute() error {
return errors.New("downloader: got bad response status from endpoint: " + res.Status)
}
// If there is a Content-Length header on this request go ahead and check that we can
// even write the whole file before beginning this process. If there is no header present
// we'll just have to give it a spin and see how it goes.
if res.ContentLength > 0 {
if err := dl.server.Filesystem().HasSpaceFor(res.ContentLength); err != nil {
return errors.WrapIf(err, "downloader: failed to write file: not enough space")
}
if res.ContentLength < 1 {
return errors.New("downloader: request is missing ContentLength")
}
if dl.req.UseHeader {
@@ -200,8 +195,10 @@ func (dl *Download) Execute() error {
p := dl.Path()
dl.server.Log().WithField("path", p).Debug("writing remote file to disk")
// Write the file while tracking the progress, Write will check that the
// size of the file won't exceed the disk limit.
r := io.TeeReader(res.Body, dl.counter(res.ContentLength))
if err := dl.server.Filesystem().Writefile(p, r); err != nil {
if err := dl.server.Filesystem().Write(p, r, res.ContentLength, 0o644); err != nil {
return errors.WrapIf(err, "downloader: failed to write file to server directory")
}
return nil

View File

@@ -45,6 +45,8 @@ func getDownloadBackup(c *gin.Context) {
return
}
// The use of `os` here is safe as backups are not stored within server
// accessible directories.
f, err := os.Open(b.Path())
if err != nil {
middleware.CaptureAndAbort(c, err)
@@ -76,26 +78,18 @@ func getDownloadFile(c *gin.Context) {
return
}
p, _ := s.Filesystem().SafePath(token.FilePath)
st, err := os.Stat(p)
// If there is an error or we're somehow trying to download a directory, just
// respond with the appropriate error.
if err != nil {
middleware.CaptureAndAbort(c, err)
return
} else if st.IsDir() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on this server.",
})
return
}
f, err := os.Open(p)
f, st, err := s.Filesystem().File(token.FilePath)
if err != nil {
middleware.CaptureAndAbort(c, err)
return
}
defer f.Close()
if st.IsDir() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on this server.",
})
return
}
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))

View File

@@ -228,6 +228,7 @@ func deleteServer(c *gin.Context) {
// In addition, servers with large amounts of files can take some time to finish deleting,
// so we don't want to block the HTTP call while waiting on this.
go func(p string) {
_ = s.Filesystem().UnixFS().Close()
if err := os.RemoveAll(p); err != nil {
log.WithFields(log.Fields{"path": p, "error": err}).Warn("failed to remove server files during deletion process")
}

View File

@@ -30,7 +30,7 @@ import (
// getServerFileContents returns the contents of a file on the server.
func getServerFileContents(c *gin.Context) {
s := middleware.ExtractServer(c)
p := "/" + strings.TrimLeft(c.Query("file"), "/")
p := strings.TrimLeft(c.Query("file"), "/")
f, st, err := s.Filesystem().File(p)
if err != nil {
middleware.CaptureAndAbort(c, err)
@@ -129,7 +129,6 @@ func putServerRenameFiles(c *gin.Context) {
}
if err := fs.Rename(pf, pt); err != nil {
// Return nil if the error is an is not exists.
// NOTE: os.IsNotExist() does not work if the error is wrapped.
if errors.Is(err, os.ErrNotExist) {
s.Log().WithField("error", err).
WithField("from_path", pf).
@@ -239,7 +238,15 @@ func postServerWriteFile(c *gin.Context) {
middleware.CaptureAndAbort(c, err)
return
}
if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
if c.Request.ContentLength < 1 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Missing Content-Length",
})
return
}
if err := s.Filesystem().Write(f, c.Request.Body, c.Request.ContentLength, 0o644); err != nil {
if filesystem.IsErrorCode(err, filesystem.ErrCodeIsDirectory) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Cannot write file, name conflicts with an existing directory by the same name.",
@@ -589,15 +596,9 @@ func postServerUploadFiles(c *gin.Context) {
}
for _, header := range headers {
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
if err != nil {
middleware.CaptureAndAbort(c, err)
return
}
// We run this in a different method so I can use defer without any of
// the consequences caused by calling it in a loop.
if err := handleFileUpload(p, s, header); err != nil {
if err := handleFileUpload(filepath.Join(directory, header.Filename), s, header); err != nil {
middleware.CaptureAndAbort(c, err)
return
} else {
@@ -619,7 +620,8 @@ func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader)
if err := s.Filesystem().IsIgnored(p); err != nil {
return err
}
if err := s.Filesystem().Writefile(p, file); err != nil {
if err := s.Filesystem().Write(p, file, header.Size, 0o644); err != nil {
return err
}
return nil

View File

@@ -106,6 +106,7 @@ func postTransfers(c *gin.Context) {
if !successful && err != nil {
// Delete all extracted files.
go func(trnsfr *transfer.Transfer) {
_ = trnsfr.Server.Filesystem().UnixFS().Close()
if err := os.RemoveAll(trnsfr.Server.Filesystem().Path()); err != nil && !os.IsNotExist(err) {
trnsfr.Log().WithError(err).Warn("failed to delete local server files")
}