CVE-2023-25168 (Go, published 2023-02-09T00:16Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: 8.2)
Weakness: CWE-59 \u2014 Improper Link Resolution Before File Access ('Link Following')
The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.
Repository: https://github.com/pterodactyl/wings
Files changed:
server/filesystem/filesystem.go
server/filesystem/filesystem_test.go
Commit message: server(filesystem): Delete tweaks


Vulnerable code:
	wg := sync.WaitGroup{}
	// not be able to delete a file that is a symlink pointing to a location outside of the data
	if st, err := os.Lstat(resolved); err != nil {
	} 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)
			}(&wg, st, resolved)
	wg.Wait()

Fixed code:
	// not be able to delete a file that is a symlink pointing to a location outside the data
	st, err := os.Lstat(resolved)
	if err != nil {
			return err

		// 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)
				break
			}

		// Always return early if the file does not exist.
		return nil
	// 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())
	}
		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()
		})