HIGH container escapegrape

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 to file:///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 image parameter is restricted to a regular expression that matches only legitimate Docker image references (no spaces, no shell metacharacters). The cmd parameter is similarly limited to a safe character set.
  • Shellword splitting: Even if a more complex command is required, Shellwords.shellsplit safely parses a string into an array without invoking a shell.
  • Library‑based execution: Using Docker::Container.create passes 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-remap or 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.

Frequently Asked Questions

Can middleBrick automatically fix the container‑escape vulnerability in my Grape API?
No. middleBrick only detects and reports security issues. It provides detailed findings and remediation guidance, but you must apply the fixes yourself in your code or configuration.
Is it safe to expose the Docker socket to a Grape API container if I validate all inputs?
Mounting the Docker socket gives the API container root‑level access to the host’s Docker daemon. Even with strict input validation, any future vulnerability or logic flaw could be exploited to escape the container. The safer approach is to avoid mounting the socket and instead use a Docker client library that communicates via a secured, API‑gated daemon or to use a dedicated orchestration layer for container operations.