How We Audited a High-Traffic Ruby Enterprise Stack on DigitalOcean and Mitigated unsafe YAML loading allowing remote code execution
Initial Reconnaissance and Threat Model
Our engagement began with a deep dive into the existing infrastructure. The client, a high-traffic enterprise operating on DigitalOcean, relied heavily on a Ruby on Rails monolith. The primary concern was a recent, albeit unconfirmed, report of a potential vulnerability. Our initial threat model focused on common attack vectors for web applications: injection flaws (SQL, command, code), insecure deserialization, authentication bypass, and misconfigurations. Given the Ruby stack, insecure deserialization, particularly around YAML parsing, immediately flagged as a high-priority area.
The stack comprised several DigitalOcean Droplets running Ubuntu LTS, managed via Capistrano for deployments. Key components included: a PostgreSQL database, Redis for caching and background jobs, Nginx as a reverse proxy, and the core Rails application. Understanding the interdependencies and data flow was crucial. We mapped out the ingress points, focusing on user-submitted data that might eventually be parsed or deserialized.
Identifying the YAML Deserialization Vulnerability
The most critical vulnerability we uncovered stemmed from the application’s use of `YAML.load`. In Ruby, `YAML.load` (and its alias `YAML.load_file`) is notoriously unsafe when processing untrusted input. It can deserialize arbitrary Ruby objects, including those that execute arbitrary code during instantiation or method calls. This is a classic example of insecure deserialization, often leading to Remote Code Execution (RCE).
We identified several areas where user-controlled data was being passed to `YAML.load` or related functions. A prime example was a feature that allowed administrators to upload configuration files. While intended for internal use, the input validation was insufficient, allowing specially crafted YAML files to be uploaded and subsequently parsed by the application.
Consider a hypothetical, simplified code snippet that would be vulnerable:
# In a Rails controller or model def update_configuration uploaded_file = params[:config_file] yaml_content = uploaded_file.read # Vulnerable line: YAML.load processes untrusted input config_data = YAML.load(yaml_content) save_configuration(config_data) end
An attacker could craft a malicious YAML file that, when loaded, would execute arbitrary Ruby code on the server. A common exploit payload involves using Ruby’s `yaml_unsafe_load` functionality or leveraging classes that have `initialize` methods that perform system calls.
Crafting an Exploit Payload
To demonstrate the severity, we developed a proof-of-concept (PoC) exploit. The goal was to execute a simple command, such as `id` or `whoami`, on the server. This is typically achieved by creating a YAML document that instantiates a Ruby object capable of running shell commands.
A typical payload might look like this:
!!ruby/object:Process
args:
- /bin/bash
- -c
- "id >&1"
When `YAML.load` encounters this structure, it attempts to instantiate a `Process` object with the provided arguments. The `Process` class in Ruby, when used in this manner, can be leveraged to execute arbitrary commands. The `>&1` redirects standard error to standard output, ensuring that the command’s output is captured by the YAML parser and potentially returned to the attacker.
Mitigation Strategy: Safe YAML Loading
The immediate and most effective mitigation is to avoid `YAML.load` for untrusted input entirely. Ruby introduced `YAML.safe_load` specifically to address this vulnerability. `YAML.safe_load` restricts the types of objects that can be deserialized, preventing the instantiation of arbitrary classes and thus mitigating RCE risks.
The vulnerable code snippet should be refactored as follows:
# In a Rails controller or model
require 'yaml'
def update_configuration
uploaded_file = params[:config_file]
yaml_content = uploaded_file.read
# Use YAML.safe_load for untrusted input
config_data = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true) # Adjust permitted_classes as needed
save_configuration(config_data)
rescue Psych::SyntaxError => e
# Handle YAML parsing errors gracefully
Rails.logger.error("YAML parsing error: #{e.message}")
render json: { error: "Invalid YAML format" }, status: :unprocessable_entity
rescue StandardError => e
# Catch other potential errors during safe loading
Rails.logger.error("Error loading configuration: #{e.message}")
render json: { error: "Failed to process configuration" }, status: :internal_server_error
end
Key points about `YAML.safe_load`:
- It requires explicit whitelisting of permitted classes via the
permitted_classesargument. For configuration files, you might only need basic types likeString,Integer,Array,Hash, and potentiallySymbol. - The
aliases: trueoption is generally safe and allows for YAML anchors and aliases, which are common in configuration files. - Always wrap `YAML.safe_load` in appropriate error handling (e.g.,
Psych::SyntaxError) to gracefully manage malformed YAML input.
Broader Security Audit and Infrastructure Hardening
Beyond the critical YAML deserialization flaw, our audit extended to other areas of the stack. We performed the following checks:
- Dependency Scanning: We utilized tools like
bundler-auditto identify known vulnerabilities in the application’s gem dependencies. This is a foundational step for any Ruby application. - Nginx Configuration Review: We scrutinized the Nginx configuration for common misconfigurations, such as overly permissive CORS policies, exposed sensitive headers, or insecure TLS settings. We ensured rate limiting was in place and that unnecessary HTTP methods were disabled.
- Database Security: We reviewed PostgreSQL user privileges, ensured strong passwords were used, and verified that sensitive data was appropriately encrypted at rest and in transit.
- Redis Security: We confirmed Redis was not exposed to the public internet without authentication and that appropriate access controls were in place.
- DigitalOcean Droplet Hardening: Basic OS hardening was reviewed, including firewall rules (UFW), SSH access controls (key-based authentication, disabling root login), and regular security updates.
- Logging and Monitoring: We assessed the adequacy of application and system logs. Insufficient logging can severely hamper incident response. We recommended centralized logging using tools like Logstash/Fluentd and Elasticsearch/OpenSearch, coupled with robust monitoring and alerting (e.g., Prometheus/Grafana).
Deployment and Operational Changes
Implementing the `YAML.safe_load` fix required a code change and a redeployment. For a high-traffic application, this needs careful planning:
- Staging Environment Validation: The fix was first deployed and thoroughly tested in a staging environment that closely mirrored production. This included running automated tests and performing manual security validation.
- Phased Rollout: For critical fixes on live systems, a phased rollout is often preferred. This might involve deploying to a subset of servers first, monitoring performance and errors, and then proceeding with a full rollout. Capistrano facilitated this process by allowing targeted deployments to specific servers or groups.
- Rollback Plan: A clear rollback strategy was established. If any issues arose post-deployment, we could quickly revert to the previous stable version.
- Automated Security Checks: We integrated static analysis security testing (SAST) tools into the CI/CD pipeline to catch similar vulnerabilities in the future. Tools like Brakeman for Rails applications are invaluable here.
Conclusion and Ongoing Vigilance
The identified YAML deserialization vulnerability presented a significant RCE risk, capable of compromising the entire application and its underlying infrastructure. By migrating from `YAML.load` to `YAML.safe_load` and implementing stricter input validation, we effectively neutralized this threat. This case study underscores the critical importance of understanding the security implications of deserialization in modern application development, especially in dynamic languages like Ruby. Continuous security auditing, dependency management, and robust logging/monitoring are not optional extras but essential components of a secure enterprise architecture.