HIGH insecure deserializationrailsmutual tls

Insecure Deserialization in Rails with Mutual Tls

Insecure Deserialization in Rails with Mutual Tls — how this specific combination creates or exposes the vulnerability

Insecure deserialization in Ruby on Rails occurs when an application reconstructs objects from untrusted data without proper validation, enabling attackers to execute code or bypass authorization. Common sources include YAML, Marshal, and JSON deserialization in parameters, background jobs, or cached data. Even when Mutual TLS (mTLS) is enforced at the transport layer, the application surface remains exposed if endpoints accept and deserialize untrusted payloads.

Mutual TLS authenticates the client and server by exchanging certificates, which prevents anonymous or spoofed connections. However, mTLS does not protect the application once a trusted TLS connection is established. For example, an authenticated client can send a serialized object crafted to exploit Rails’ deserialization routines. Because mTLS is often used in service-to-service communication and API gateways, developers may mistakenly assume the channel is secure and skip input validation. This assumption is dangerous: mTLS stops network-level impersonation but does not prevent malicious payloads carried over an authorized TLS session.

Consider a Rails API that uses client certificates to authorize requests. The endpoint might parse incoming JSON containing serialized objects or YAML fragments for legacy integration. If the parameter is passed to YAML.safe_load without restricting permitted classes, or uses Ruby’s Marshal.load on user-controlled data, an attacker can embed gadget chains that execute arbitrary code during deserialization. Real-world CVEs such as CVE-2013-0156 demonstrate how unsafe deserialization in Rails led to remote code execution via crafted parameters. In a mTLS-enabled service, an attacker who obtains a valid client certificate (through mismanagement or compromise) can deliver the same payload over an encrypted channel, increasing the likelihood of bypassing network-based detection.

SSRF and outbound connections are also relevant. An mTLS-enabled Rails service might deserialize URLs supplied by a client; if the service then fetches those URLs, it can be forced to reach internal resources. Combined with insecure deserialization, this can turn the Rails app into a pivot point for internal network attacks. The key takeaway is that mTLS secures transport and identity, but it does not replace secure deserialization practices; input validation, class restrictions, and runtime scanning remain essential.

Mutual Tls-Specific Remediation in Rails — concrete code fixes

Remediation centers on strict deserialization controls and proper mTLS configuration. Never deserialize untrusted data using Marshal.load or permissive YAML loading. Instead, use safe parsing and explicitly allow only expected classes. Below are concrete, working examples for a Rails controller and initializer that integrate mTLS and secure deserialization.

Enabling Mutual TLS in Rails

Configure your web server (e.g., Puma with SSL) to require client certificates. In config/puma.rb, set the SSL options to verify mode and provide the trusted CA bundle:

# config/puma.rb
ssl_bind '0.0.0.0', '8443', {
  key: '/path/to/server.key',
  cert: '/path/to/server.crt',
  ca_file: '/path/to/ca_bundle.crt',
  verify_mode: 'verify_peer' # Enforce client certificate verification
}

This ensures that only clients presenting certificates signed by the trusted CA can establish a TLS connection. Rails will receive the client certificate in request.env['SSL_CLIENT_CERT'] or via request.headers['X-SSL-CERT'] depending on your proxy setup.

Secure Deserialization with Allowed Classes

Use YAML.safe_load with an explicit whitelist and avoid Marshal for untrusted input. For example, in a controller that accepts JSON containing nested attributes, parse safely:

# app/controllers/api/data_controller.rb
class Api::DataController < ApplicationController
  # Only allow basic scalar types and known safe classes
  ALLOWED_CLASSES = [Symbol, String, Integer, Float, TrueClass, FalseClass, NilClass, Array, Hash].freeze

  def create
    payload = params[:payload] || request.body.read
    begin
      data = YAML.safe_load(payload, permitted_classes: ALLOWED_CLASSES)
      # Process validated data
      render json: { status: 'ok', data: data.slice('name', 'value') }
    rescue Psych::DisallowedClass => e
      render json: { error: 'Disallowed class in YAML' }, status: :bad_request
    rescue Psych::SyntaxError => e
      render json: { error: 'Invalid YAML syntax' }, status: :bad_request
    end
  end
end

If you must deserialize Ruby objects, use a hardened approach with SafeYAML or restrict YAML to safe loading. Avoid Marshal.load entirely for external data.

Validating Client Certificates in Application Logic

After mTLS at the transport layer, verify certificate details in your code to enforce additional policies (e.g., CN or SAN checks):

# app/controllers/concerns/mtls_authenticatable.rb
module MtlsAuthenticatable
  extend ActiveSupport::Concern

  def verify_client_certificate
    cert_pem = request.env['SSL_CLIENT_CERT']
    return head :forbidden unless cert_pem

    cert = OpenSSL::X509::Certificate.new(cert_pem)
    # Example: ensure certificate common name matches an allowed service
    unless cert.subject.to_s.include?('CN=trusted-service')
      render json: { error: 'Unauthorized certificate' }, status: :forbidden
    end
  rescue OpenSSL::X509::CertificateError
    render json: { error: 'Invalid certificate' }, status: :forbidden
  end
end

Combine this with route-level before_actions to ensure sensitive endpoints are protected. These measures ensure that even with mTLS, deserialization is bounded and identity checks are explicit.

Frequently Asked Questions

Does Mutual TLS prevent insecure deserialization vulnerabilities?
No. Mutual TLS authenticates endpoints and encrypts traffic, but it does not stop an authorized client from sending malicious serialized payloads. Secure deserialization controls are still required.
What should I allow in YAML.safe_load to minimize risk in Rails?
Permit only basic scalar classes such as String, Integer, Float, Symbol, TrueClass, FalseClass, NilClass, Array, and Hash. Avoid permitting Class or allowing arbitrary object deserialization.