CVE-2021-32699: Allocation of Resources Without Limits or Throttling (CWE-770)
value: Wings is the control plane software for the open source Pterodactyl game management system. All versions of Pterodactyl Wings prior to `1.4.4` are vulnerable to system resource exhaustion due to improper container process limits being defined. A malicious user can consume more resources than intended and cause downstream impacts to other clients on the same hardware, eventually causing the physical server to stop responding. Users should upgrade to `1.4.4` to mitigate the issue. There is no non-code based workaround for impacted versions of the software. Users running customized versions of this software can manually set a PID limit for containers created.
CWE description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
CVSS v3 base score: 6.5
Published: 2021-06-22T20:15Z
Repository: https://github.com/pterodactyl/wings
Language: Go
Files changed: config/config_docker.go, environment/docker/container.go, environment/settings.go
Commit date: 2021-06-20 16:54:00 -0700
Commit message: [security] enforce process limits at a per-container level to avoid abusive clients impacting other instances

Diff: diff --git a/config/config_docker.go b/config/config_docker.go
index 3db5a56..11d4f59 100644
--- a/config/config_docker.go
+++ b/config/config_docker.go
@@ -53,10 +53,16 @@ type DockerConfiguration struct {
 
 	// The size of the /tmp directory when mounted into a container. Please be aware that Docker
 	// utilizes host memory for this value, and that we do not keep track of the space used here
 	// so avoid allocating too much to a server.
 	TmpfsSize uint `default:"100" json:"tmpfs_size" yaml:"tmpfs_size"`
+
+	// ContainerPidLimit sets the total number of processes that can be active in a container
+	// at any given moment. This is a security concern in shared-hosting environments where a
+	// malicious process could create enough processes to cause the host node to run out of
+	// available pids and crash.
+	ContainerPidLimit int64 `default:"256" json:"container_pid_limit" yaml:"container_pid_limit"`
 }
 
 // RegistryConfiguration defines the authentication credentials for a given
 // Docker registry.
 type RegistryConfiguration struct {
diff --git a/environment/docker/container.go b/environment/docker/container.go
index 0ca5256..df66ce4 100644
--- a/environment/docker/container.go
+++ b/environment/docker/container.go
@@ -484,10 +484,11 @@ func (e *Environment) convertMounts() []mount.Mount {
 	return out
 }
 
 func (e *Environment) resources() container.Resources {
 	l := e.Configuration.Limits()
+	pids := l.ProcessLimit()
 
 	return container.Resources{
 		Memory:            l.BoundedMemoryLimit(),
 		MemoryReservation: l.MemoryLimit * 1_000_000,
 		MemorySwap:        l.ConvertedSwap(),
@@ -495,7 +496,8 @@ func (e *Environment) resources() container.Resources {
 		CPUPeriod:         100_000,
 		CPUShares:         1024,
 		BlkioWeight:       l.IoWeight,
 		OomKillDisable:    &l.OOMDisabled,
 		CpusetCpus:        l.Threads,
+		PidsLimit:         &pids,
 	}
 }
diff --git a/environment/settings.go b/environment/settings.go
index 1878eb0..e3e40dd 100644
--- a/environment/settings.go
+++ b/environment/settings.go
@@ -4,10 +4,11 @@ import (
 	"fmt"
 	"math"
 	"strconv"
 
 	"github.com/apex/log"
+	"github.com/pterodactyl/wings/config"
 )
 
 type Mount struct {
 	// In Docker environments this makes no difference, however in a non-Docker environment you
 	// should treat the "Default" mount as the root directory for the server. All other mounts
@@ -26,12 +27,12 @@ type Mount struct {
 	// Whether or not the directory is being mounted as read-only. It is up to the environment to
 	// handle this value correctly and ensure security expectations are met with its usage.
 	ReadOnly bool `json:"read_only"`
 }
 
-// The build settings for a given server that impact docker container creation and
-// resource limits for a server instance.
+// Limits is the build settings for a given server that impact docker container
+// creation and resource limits for a server instance.
 type Limits struct {
 	// The total amount of memory in megabytes that this server is allowed to
 	// use on the host system.
 	MemoryLimit int64 `json:"memory_limit"`
 
@@ -54,24 +55,25 @@ type Limits struct {
 	Threads string `json:"threads"`
 
 	OOMDisabled bool `json:"oom_disabled"`
 }
 
-// Converts the CPU limit for a server build into a number that can be better understood
-// by the Docker environment. If there is no limit set, return -1 which will indicate to
-// Docker that it has unlimited CPU quota.
+// ConvertedCpuLimit converts the CPU limit for a server build into a number
+// that can be better understood by the Docker environment. If there is no limit
+// set, return -1 which will indicate to Docker that it has unlimited CPU quota.
 func (r *Limits) ConvertedCpuLimit() int64 {
 	if r.CpuLimit == 0 {
 		return -1
 	}
 
 	return r.CpuLimit * 1000
 }
 
-// Set the hard limit for memory usage to be 5% more than the amount of memory assigned to
-// the server. If the memory limit for the server is < 4G, use 10%, if less than 2G use
-// 15%. This avoids unexpected crashes from processes like Java which run over the limit.
+// MemoryOverheadMultiplier sets the hard limit for memory usage to be 5% more
+// than the amount of memory assigned to the server. If the memory limit for the
+// server is < 4G, use 10%, if less than 2G use 15%. This avoids unexpected
+// crashes from processes like Java which run over the limit.
 func (r *Limits) MemoryOverheadMultiplier() float64 {
 	if r.MemoryLimit <= 2048 {
 		return 1.15
 	} else if r.MemoryLimit <= 4096 {
 		return 1.10
@@ -82,27 +84,34 @@ func (r *Limits) MemoryOverheadMultiplier() float64 {
 
 func (r *Limits) BoundedMemoryLimit() int64 {
 	return int64(math.Round(float64(r.MemoryLimit) * r.MemoryOverheadMultiplier() * 1_000_000))
 }
 
-// Returns the amount of swap available as a total in bytes. This is returned as the amount
-// of memory available to the server initially, PLUS the amount of additional swap to include
-// which is the format used by Docker.
+// ConvertedSwap returns the amount of swap available as a total in bytes. This
+// is returned as the amount of memory available to the server initially, PLUS
+// the amount of additional swap to include which is the format used by Docker.
 func (r *Limits) ConvertedSwap() int64 {
 	if r.Swap < 0 {
 		return -1
 	}
 
 	return (r.Swap * 1_000_000) + r.BoundedMemoryLimit()
 }
 
+// ProcessLimit returns the process limit for a container. This is currently
+// defined at a system level and not on a per-server basis.
+func (r *Limits) ProcessLimit() int64 {
+	return config.Get().Docker.ContainerPidLimit
+}
+
 type Variables map[string]interface{}
 
-// Ugly hacky function to handle environment variables that get passed through as not-a-string
-// from the Panel. Ideally we'd just say only pass strings, but that is a fragile idea and if a
-// string wasn't passed through you'd cause a crash or the server to become unavailable. For now
-// try to handle the most likely values from the JSON and hope for the best.
+// Get is an ugly hacky function to handle environment variables that get passed
+// through as not-a-string from the Panel. Ideally we'd just say only pass
+// strings, but that is a fragile idea and if a string wasn't passed through
+// you'd cause a crash or the server to become unavailable. For now try to
+// handle the most likely values from the JSON and hope for the best.
 func (v Variables) Get(key string) string {
 	val, ok := v[key]
 	if !ok {
 		return ""
 	}

Vulnerable code: // The build settings for a given server that impact docker container creation and
// resource limits for a server instance.
// Converts the CPU limit for a server build into a number that can be better understood
// by the Docker environment. If there is no limit set, return -1 which will indicate to
// Docker that it has unlimited CPU quota.
// Set the hard limit for memory usage to be 5% more than the amount of memory assigned to
// the server. If the memory limit for the server is < 4G, use 10%, if less than 2G use
// 15%. This avoids unexpected crashes from processes like Java which run over the limit.
// Returns the amount of swap available as a total in bytes. This is returned as the amount
// of memory available to the server initially, PLUS the amount of additional swap to include
// which is the format used by Docker.
// Ugly hacky function to handle environment variables that get passed through as not-a-string
// from the Panel. Ideally we'd just say only pass strings, but that is a fragile idea and if a
// string wasn't passed through you'd cause a crash or the server to become unavailable. For now
// try to handle the most likely values from the JSON and hope for the best.
Fixed code: 
	// ContainerPidLimit sets the total number of processes that can be active in a container
	// at any given moment. This is a security concern in shared-hosting environments where a
	// malicious process could create enough processes to cause the host node to run out of
	// available pids and crash.
	ContainerPidLimit int64 `default:"256" json:"container_pid_limit" yaml:"container_pid_limit"`
	pids := l.ProcessLimit()
		PidsLimit:         &pids,
	"github.com/pterodactyl/wings/config"
// Limits is the build settings for a given server that impact docker container
// creation and resource limits for a server instance.
// ConvertedCpuLimit converts the CPU limit for a server build into a number
// that can be better understood by the Docker environment. If there is no limit
// set, return -1 which will indicate to Docker that it has unlimited CPU quota.
// MemoryOverheadMultiplier sets the hard limit for memory usage to be 5% more
// than the amount of memory assigned to the server. If the memory limit for the
// server is < 4G, use 10%, if less than 2G use 15%. This avoids unexpected
// crashes from processes like Java which run over the limit.
// ConvertedSwap returns the amount of swap available as a total in bytes. This
// is returned as the amount of memory available to the server initially, PLUS
// the amount of additional swap to include which is the format used by Docker.
// ProcessLimit returns the process limit for a container. This is currently
// defined at a system level and not on a per-server basis.
func (r *Limits) ProcessLimit() int64 {
	return config.Get().Docker.ContainerPidLimit
}

// Get is an ugly hacky function to handle environment variables that get passed
// through as not-a-string from the Panel. Ideally we'd just say only pass
// strings, but that is a fragile idea and if a string wasn't passed through
// you'd cause a crash or the server to become unavailable. For now try to
// handle the most likely values from the JSON and hope for the best.