Container Escape in Grape
How Container Escape Manifests in Grape
Grape is a lightweight Ruby framework for building APIs, and like any API framework it can inadvertently expose dangerous system calls when developers pass user‑supplied data directly to shell commands or privileged utilities. A common pattern is an endpoint that accepts parameters such as an image name or a command and then executes a Docker command via backticks, Kernel#system, or Open3.popen3 without proper sanitisation.
Example of vulnerable Grape code:
class DockerAPI < Grape::API
format :json
resource :docker do
desc "Run a container"
params do
requires :image, type: String, desc => "Docker image"
requires :cmd, type: String, desc => "Command to run inside the container"
end
post :run do
# DANGEROUS: direct interpolation into a shell command
result = `docker run --rm #{params[:image]} #{params[:cmd]}`
{ output: result }
end
end
end
If an attacker controls params[:image] or params[:cmd], they can inject shell metacharacters (e.g., ;, &&, |, $()) to break out of the intended command. In a containerised deployment where the host’s Docker socket (/var/run/docker.sock) is mounted into the API container, the attacker can then execute commands such as:
image: "alpine" cmd: "-v /var/run/docker.sock:/var/run/docker.sock alpine sh -c 'docker run --rm -v /:/host alpine chroot /host /bin/bash'"– this starts a new container with the host’s root filesystem mounted, achieving a full container escape.image: "alpine" cmd: "; cat /etc/passwd"– while not a full escape, it shows that arbitrary host commands can be run if the socket is accessible.
Because the API runs as the same user that owns the Docker socket, the injected command inherits the ability to talk directly to the Docker daemon, which can then be instructed to start privileged containers with host mounts, effectively escaping the isolation boundary.
Grape‑Specific Detection — How to Identify This Issue, Including Scanning with middleBrick
middleBrick performs unauthenticated, black‑box testing of an API’s surface. It does not need source code or agents; it simply sends a series of crafted requests to the endpoint and analyses the responses for signs of successful command execution or data leakage.
During the Input Validation and SSRF checks (two of the twelve parallel scans), middleBrick will:
- Send payloads that contain shell metacharacters (e.g.,
; id,$(whoami),`ls /`) within parameters that the API expects to be used in a command. - Look for responses that contain the output of those commands (e.g., uid/gid information, directory listings) or error messages that reveal command execution.
- Attempt to reach internal services via the API if it proxies requests; if the API can be made to forward a request to
http://localhost:2375(the Docker daemon socket exposed over TCP) or tofile:///var/run/docker.sock, the SSRF check will flag a successful reach.
Example of using the middleBrick CLI to test a Grape API:
$ middlebrick scan https://api.example.com/docker/run
Scanning... (5‑15 seconds)
Security Score: D (42/100)
Findings:
- Input Validation: High – Shell injection detected in `image` parameter. Payload `; id` returned uid=0(root) gid=0(root).
- SSRF: Medium – Able to reach internal service at `http://localhost:2375/version` via the `cmd` parameter.
Remediation Guidance:
* Avoid interpolating user input into shell commands.
* Use an API‑native Docker client (e.g., the `docker-api` gem) and pass arguments as an array.
* Implement strict whitelists for image names and allowed commands.
Because middleBrick does not require credentials or configuration, the test can be run against any publicly exposed Grape endpoint (staging, production, or a temporary review app) and will surface the exact injection vectors that could lead to a container escape.
Grape‑Specific Remediation — Code Fixes Using Grape’s Native Features/Libraries
The safest way to eliminate the container‑escape risk is to remove the shell altogether and interact with Docker through a library that accepts arguments as an array, thus preventing any chance of shell metacharacter interpretation. The docker-api gem is officially supported and works well within a Grape endpoint.
Revised Grape code using docker-api with input validation:
require 'docker'
require 'grape'
class DockerAPI < Grape::API
format :json
# Initialize Docker client (defaults to unix socket /var/run/docker.sock)
Docker.url = ENV['DOCKER_URL'] || 'unix:///var/run/docker.sock'
resource :docker do
desc "Run a container safely"
params do
requires :image, type: String, regexp: /^[a-z0-9]+(?:[._-][a-z0-9]+)*(:[a-zA-Z0-9_.-]+)?$/, desc => "Docker image name (alphanumeric, optional tag)"
optional :cmd, type: String, regexp: /^[a-zA-Z0-9 .,-]+$/, desc => "Command to run (limited to safe characters)"
end
post :run do
image = params[:image]
cmd = params[:cmd] || ['sh']
# Split command into an array for execve-style execution
cmd_array = cmd.shellsplit # uses Shellwords.shellsplit under the hood
begin
container = Docker::Container.create(
'Image' => image,
'Cmd' => cmd_array,
'AttachStdout' => true,
'AttachStderr' => true
)
container.start
output = container.wait
{ status: output['StatusCode'], logs: container.logs(stdout: true, stderr: true) }
rescue Docker::Error::DockerError => e
error!({ error: e.message }, 500)
end
end
end
end
Key remediation points:
- Whitelist validation: The
imageparameter is restricted to a regular expression that matches only legitimate Docker image references (no spaces, no shell metacharacters). Thecmdparameter is similarly limited to a safe character set. - Shellword splitting: Even if a more complex command is required,
Shellwords.shellsplitsafely parses a string into an array without invoking a shell. - Library‑based execution: Using
Docker::Container.createpasses the command as an array directly to the Docker daemon’s execve call, eliminating any shell interpretation. - Principle of least privilege: Run the API container as a non‑root user, drop unnecessary Linux capabilities, and avoid mounting the host Docker socket unless absolutely required. If the socket must be mounted, ensure the API container runs with a restricted user ID and consider using
--userns-remapor SELinux/AppArmor profiles to limit what the daemon can do.
After applying these changes, a middleBrick rescan will show the Input Validation and SSRF findings resolved, and the security score will improve (e.g., from D to B). The API now safely accepts user input without exposing a path to container escape.