VYPR
Critical severityNVD Advisory· Published Feb 8, 2023· Updated Mar 10, 2025

Symbolic Link (Symlink) Following allowing the deletion of files and directories on the host system in wings

CVE-2023-25168

Description

Wings is Pterodactyl's server control plane. This vulnerability can be used to delete files and directories recursively on the host system. This vulnerability can be combined with GHSA-p8r3-83r8-jwj5 to overwrite files on the host system. In order to use this exploit, an attacker must have an existing "server" allocated and controlled by Wings. This vulnerability has been resolved in version v1.11.4 of Wings, and has been back-ported to the 1.7 release series in v1.7.4. Anyone running v1.11.x should upgrade to v1.11.4 and anyone running v1.7.x should upgrade to v1.7.4. There are no known workarounds for this issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/pterodactyl/wingsGo
< 1.7.41.7.4
github.com/pterodactyl/wingsGo
>= 1.11.0, < 1.11.41.11.4

Affected products

1

Patches

1
429ac62dba22

server(filesystem): Delete tweaks

https://github.com/pterodactyl/wingsMatthew PennerFeb 8, 2023via ghsa
2 files changed · +127 14
  • server/filesystem/filesystem.go+53 14 modified
    @@ -387,10 +387,9 @@ func (fs *Filesystem) TruncateRootDirectory() error {
     // Delete removes a file or folder from the system. Prevents the user from
     // accidentally (or maliciously) removing their root server data directory.
     func (fs *Filesystem) Delete(p string) error {
    -	wg := sync.WaitGroup{}
     	// This is one of the few (only?) places in the codebase where we're explicitly not using
     	// the SafePath functionality when working with user provided input. If we did, you would
    -	// not be able to delete a file that is a symlink pointing to a location outside of the data
    +	// not be able to delete a file that is a symlink pointing to a location outside the data
     	// directory.
     	//
     	// We also want to avoid resolving a symlink that points _within_ the data directory and thus
    @@ -407,25 +406,65 @@ func (fs *Filesystem) Delete(p string) error {
     		return errors.New("cannot delete root server directory")
     	}
     
    -	if st, err := os.Lstat(resolved); err != nil {
    +	st, err := os.Lstat(resolved)
    +	if err != nil {
     		if !os.IsNotExist(err) {
     			fs.error(err).Warn("error while attempting to stat file before deletion")
    +			return err
     		}
    -	} else {
    -		if !st.IsDir() {
    -			fs.addDisk(-st.Size())
    -		} else {
    -			wg.Add(1)
    -			go func(wg *sync.WaitGroup, st os.FileInfo, resolved string) {
    -				defer wg.Done()
    -				if s, err := fs.DirectorySize(resolved); err == nil {
    -					fs.addDisk(-s)
    +
    +		// The following logic is used to handle a case where a user attempts to
    +		// delete a file that does not exist through a directory symlink.
    +		// We don't want to reveal that the file does not exist, so we validate
    +		// the path of the symlink and return a bad path error if it is invalid.
    +
    +		// The requested file or directory doesn't exist, so at this point we
    +		// need to iterate up the path chain until we hit a directory that
    +		// _does_ exist and can be validated.
    +		parts := strings.Split(filepath.Dir(resolved), "/")
    +
    +		// Range over all the path parts and form directory paths from the end
    +		// moving up until we have a valid resolution, or we run out of paths to
    +		// try.
    +		for k := range parts {
    +			try := strings.Join(parts[:(len(parts)-k)], "/")
    +			if !fs.unsafeIsInDataDirectory(try) {
    +				break
    +			}
    +
    +			t, err := filepath.EvalSymlinks(try)
    +			if err == nil {
    +				if !fs.unsafeIsInDataDirectory(t) {
    +					return NewBadPathResolution(p, t)
     				}
    -			}(&wg, st, resolved)
    +				break
    +			}
     		}
    +
    +		// Always return early if the file does not exist.
    +		return nil
     	}
     
    -	wg.Wait()
    +	// If the file is not a symlink, we need to check that it is not within a
    +	// symlinked directory that points outside the data directory.
    +	if st.Mode()&os.ModeSymlink == 0 {
    +		ep, err := filepath.EvalSymlinks(resolved)
    +		if err != nil {
    +			if !os.IsNotExist(err) {
    +				return err
    +			}
    +		} else if !fs.unsafeIsInDataDirectory(ep) {
    +			return NewBadPathResolution(p, ep)
    +		}
    +	}
    +
    +	if st.IsDir() {
    +		if s, err := fs.DirectorySize(resolved); err == nil {
    +			fs.addDisk(-s)
    +		}
    +	} else {
    +		fs.addDisk(-st.Size())
    +	}
     
     	return os.RemoveAll(resolved)
     }
    
  • server/filesystem/filesystem_test.go+74 0 modified
    @@ -537,6 +537,80 @@ func TestFilesystem_Delete(t *testing.T) {
     			}
     		})
     
    +		g.It("deletes a symlink but not it's target within the root directory", func() {
    +			// Symlink to a file inside the root directory.
    +			err := os.Symlink(filepath.Join(rfs.root, "server/source.txt"), filepath.Join(rfs.root, "server/symlink.txt"))
    +			g.Assert(err).IsNil()
    +
    +			// Delete the symlink itself.
    +			err = fs.Delete("symlink.txt")
    +			g.Assert(err).IsNil()
    +
    +			// Ensure the symlink was deleted.
    +			_, err = os.Lstat(filepath.Join(rfs.root, "server/symlink.txt"))
    +			g.Assert(err).IsNotNil()
    +
    +			// Ensure the symlink target still exists.
    +			_, err = os.Lstat(filepath.Join(rfs.root, "server/source.txt"))
    +			g.Assert(err).IsNil()
    +		})
    +
    +		g.It("does not delete files symlinked outside of the root directory", func() {
    +			// Create a file outside the root directory.
    +			err := rfs.CreateServerFileFromString("/../source.txt", "test content")
    +			g.Assert(err).IsNil()
    +
    +			// Create a symlink to the file outside the root directory.
    +			err = os.Symlink(filepath.Join(rfs.root, "source.txt"), filepath.Join(rfs.root, "/server/symlink.txt"))
    +			g.Assert(err).IsNil()
    +
    +			// Delete the symlink. (This should pass as we will delete the symlink itself, not it's target)
    +			err = fs.Delete("symlink.txt")
    +			g.Assert(err).IsNil()
    +
    +			// Ensure the file outside the root directory still exists.
    +			_, err = os.Lstat(filepath.Join(rfs.root, "source.txt"))
    +			g.Assert(err).IsNil()
    +		})
    +
    +		g.It("does not delete files symlinked through a directory outside of the root directory", func() {
    +			// Create a directory outside the root directory.
    +			err := os.Mkdir(filepath.Join(rfs.root, "foo"), 0o755)
    +			g.Assert(err).IsNil()
    +
    +			// Create a file inside the directory that is outside the root.
    +			err = rfs.CreateServerFileFromString("/../foo/source.txt", "test content")
    +			g.Assert(err).IsNil()
    +
    +			// Symlink the directory that is outside the root to a file inside the root.
    +			err = os.Symlink(filepath.Join(rfs.root, "foo"), filepath.Join(rfs.root, "server/symlink"))
    +			g.Assert(err).IsNil()
    +
    +			// Delete a file inside the symlinked directory.
    +			err = fs.Delete("symlink/source.txt")
    +			g.Assert(err).IsNotNil()
    +			g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
    +
    +			// Ensure the file outside the root directory still exists.
    +			_, err = os.Lstat(filepath.Join(rfs.root, "foo/source.txt"))
    +			g.Assert(err).IsNil()
    +		})
    +
    +		g.It("returns an error when trying to delete a non-existent file symlinked through a directory outside of the root directory", func() {
    +			// Create a directory outside the root directory.
    +			err := os.Mkdir(filepath.Join(rfs.root, "foo2"), 0o755)
    +			g.Assert(err).IsNil()
    +
    +			// Symlink the directory that is outside the root to a file inside the root.
    +			err = os.Symlink(filepath.Join(rfs.root, "foo2"), filepath.Join(rfs.root, "server/symlink"))
    +			g.Assert(err).IsNil()
    +
    +			// Delete a file inside the symlinked directory.
    +			err = fs.Delete("symlink/source.txt")
    +			g.Assert(err).IsNotNil()
    +			g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
    +		})
    +
     		g.AfterEach(func() {
     			rfs.reset()
     
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.