Memory allocation injections occur when an application computes the size of a piece of memory to be allocated from an untrusted source. In such a
case, an attacker could be able to make the application unwillingly consume an important amount of memory by enforcing a large allocation size.
Why is this an issue?
By repeatedly requesting a feature that consumes a lot of memory, attackers can constantly occupy an important part of an application’s hosting
server memory. Depending on the application’s deployment architecture, hosting server resources and attackers' capabilities, this can lead to an
exhaustion of the available server’s memory.
What is the potential impact?
A server that faces a memory exhaustion situation can become unstable. The exact impact will depend on how the affected application is deployed and
how well the hosting server configuration is hardened.
In the worst case, when the application is deployed in an uncontained environment, directly on its host system, the memory exhaustion will affect
the whole hosting server. The server’s operating system might start killing arbitrary memory-intensive processes, including the main application or
other sensitive ones. This will result in a general operating failure, also known as a Denial of Service (DoS).
In cases where the application is deployed in a virtualized or otherwise contained environment, or where memory usage limits are in place, the
consequences are limited to the vulnerable application only. In that case, other processes and applications hosted on the same server may keep on
running without perturbation. The mainly affected application will still stop working properly.
In general, that kind of DoS attack can have severe financial consequences. They are particularly important when the affected systems are
business-critical.
How to fix it in .NET
Code examples
The following code is vulnerable to a memory allocation injection because the size of a memory allocation is determined using a user-controlled
source. It then performs the actual allocation without any verification or other sanitization over the provided size.
Noncompliant code example
[Route("NonCompliantArrayList")]
public string NonCompliantArrayList()
{
int size;
try
{
size = int.Parse(Request.Query["size"]);
}
catch (FormatException)
{
return "Number format exception while reading size";
}
ArrayList arrayList = new ArrayList(size); // Noncompliant
return size + " bytes were allocated.";
}
Compliant solution
public const int MAX_ALLOC_SIZE = 1024;
[Route("CompliantArrayList")]
public string CompliantArrayList()
{
int size;
try
{
size = int.Parse(Request.Query["size"]);
}
catch (FormatException)
{
return "Number format exception while reading size";
}
size = Math.Min(size, MAX_ALLOC_SIZE);
ArrayList arrayList = new ArrayList(size);
return size + " bytes were allocated.";
}
How does this work?
Enforce an upper limit
When performing a memory allocation whose size depends on a user-controlled parameter, it is of prime importance to enforce an upper limit to the
size being allocated. This will prevent any overly big memory slot from being consumed by a single allocation.
Note that forcing an upper limit will not prevent Denial of Service attacks. When an allocation size is restricted to a reasonable amount,
attackers can still request the allocating feature multiple times until the combined allocation size becomes big enough to cause exhaustion. However,
the smaller the allowed allocation size, the higher the number of necessary requests and, thus, the higher the required resources on the attacker
side. As for most of the DoS attack vectors, a trade-off must be found to prevent most attackers from causing exhaustion while keeping a good level of
performance and usability.
Here, the example compliant code uses the Math.Min
function to enforce a reasonable upper bound to the allocation size. In that case,
no more than 1024 bytes can be allocated at a time.
Harden the execution environment configuration
As a defense in depth measure, it is advised to harden the execution environment configuration regarding memory usage. This can effectively reduce
the scope of a successful Denial of Service attack and prevent a complete outage, potentially ranging over multiple applications.
When running the application in a contained environment, like a Docker container, it is usually possible to limit the amount of memory provided to
the contained environment. In that case, memory exhaustion will only impact the application hosting container and not the host system.
When running the application directly on a physical or heavy virtualized server, memory limits can sometimes be set on the application’s associated
service account. For example, the ulimit
mechanism of Unix based operating systems can be used for that purpose. With such a limit set
up, memory exhaustion only impacts the applications and processes owned by the related service account.
Resources
Documentation
- OWASP - Denial of Service
- Linux.org - pam_limits - PAM module to limit resources
- RedHat - How to set limits for services in RHEL and systemd
Standards