Container Escape in Rails
How Container Escape Manifests in Rails
Container escape occurs when an attacker breaks out of the isolated runtime environment (e.g., Docker, containerd) and gains elevated privileges on the host. In a Rails application, the most common path to this outcome is through uncontrolled command execution that lets the attacker interact with the container runtime or the underlying host.
Typical vulnerable patterns include:
- Direct use of
Kernel#system,`backticks`, orOpen3.popen3with unsanitized user input. - Rails controllers or background jobs that expose a debugging endpoint accepting a
commandparameter and passing it tosystem. - Misconfigured Docker mounts that expose the host’s Docker socket (
/var/run/docker.sock) inside the container; if the Rails app can issue Docker API calls, it can start a privileged container on the host. - Use of
rails consoleorraketasks that invokeexecwith user‑supplied arguments without validation.
Real‑world examples illustrate the risk. CVE‑2019‑5736 (runc) allowed a malicious container to overwrite the host runc binary and obtain root access. While the vulnerability lies in the container runtime, a Rails app that can execute arbitrary commands inside the container can trigger the exploit by, for example, writing a crafted binary to /proc/self/exe. Another relevant case is CVE‑2021‑22986 in Fluentd, where insufficient input validation led to remote code execution; a Rails logger that forwards unsanitized parameters to Fluentd could be leveraged similarly.
From an API security perspective, this maps to OWASP API Security Top 10 2023 A6: Security Misconfiguration, because the exposure typically stems from excessive privileges, unnecessary mounts, or lax input validation rather than a flaw in the business logic itself.
Rails‑Specific Detection
Detecting a container‑escape‑prone Rails API requires looking for two complementary signals: (1) the ability to execute arbitrary OS commands via user‑controlled input, and (2) the presence of privileged container capabilities such as a mounted Docker socket or --privileged flag.
middleBrick performs unauthenticated, black‑box scanning that probes each endpoint with a variety of payloads. For command injection, it sends strings like ; id, || whoami, and `sleep 5` in parameters, headers, and body fields. If the response timing or output indicates successful execution, middleBrick flags the finding under the “Input Validation” check with a severity of high.
For Docker‑socket exposure, middleBrick attempts to connect to the Unix domain socket exposed over HTTP (e.g., via a side‑car that forwards /var/run/docker.sock to a TCP port) or checks for HTTP endpoints that return Docker API version information (/version). If such an interface is reachable, the scanner raises a finding under the “SSRF” and “Data Exposure” categories, noting that an attacker could issue Docker commands to start a privileged container on the host.
Example CLI usage:
# Install the middleBrick CLI (npm package)
npm i -g middlebrick
# Scan a Rails API endpoint
middlebrick scan https://rails-api.example.com/containers
The output includes a JSON report with a per‑category breakdown, prioritized findings, and remediation guidance. Because the scan takes only 5–15 seconds and requires no agents or credentials, it can be run locally, in CI pipelines, or via the GitHub Action to catch regressions before deployment.
Rails‑Specific Remediation
The most effective mitigation is to eliminate the ability for untrusted input to reach OS‑command execution primitives. Rails provides several built‑in mechanisms to achieve this safely.
1. **Never pass user data directly to system, backticks, or Open3.** Use an allowlist of permitted commands or, better, replace shell calls with Ruby libraries that perform the same work without invoking a shell. For example, instead of:
class DebugController < ApplicationController
def run
system(params[:cmd]) # DANGEROUS
end
end
use a whitelisted approach:
class DebugController < ApplicationController
ALLOWED = { 'list' => 'ls -la', 'pwd' => 'pwd' }
def run
cmd = ALLOWED[params[:cmd]]
if cmd
# Use Open3 without a shell
stdout, stderr, status = Open3.capture2e(*Shellwords.split(cmd))
render plain: stdout
else
head :bad_request
end
end
end
2. **Avoid mounting the Docker socket inside the container.** If the Rails image must interact with Docker, use a dedicated side‑car service that runs with the minimum required privileges and communicates via a well‑defined API (e.g., HTTP/JSON) rather than sharing /var/run/docker.sock. In your Dockerfile or compose file, omit:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
3. **Run the container as a non‑root user and drop unnecessary Linux capabilities.** A Dockerfile snippet:
FROM ruby:3.2
RUN useradd -m railsapp
USER railsapp
# ... rest of the image
4. **Limit the container’s filesystem to read‑only where possible** and add an explicit --tmpfs /tmp to prevent persistence of malicious binaries.
5. **Leverage Rails security features** such as strong_parameters to whitelist inputs, and ActiveSupport::MessageVerifier or signed cookies when you need to pass trusted tokens between services.
By applying these fixes, the attack surface that could lead to a container escape is removed, and middleBrick will no longer report command‑injection or Docker‑socket findings for the affected endpoints.