Xml External Entities in Rails with Mutual Tls
Xml External Entities in Rails with Mutual Tls — how this specific combination creates or exposes the vulnerability
XML External Entity (XXE) is a class of injection flaw that occurs when an application processes XML input containing references to external entities. In Ruby on Rails, this typically surfaces when developers use libraries such as Nokogiri to parse XML without disabling external entity resolution. A Rails app that also enforces Mutual TLS (mTLS) introduces a distinct interaction: mTLS ensures that only clients presenting a valid client certificate can reach the endpoint, but it does not inherently limit what the server does with the XML body once the TLS handshake completes.
When mTLS is in place, the effective attack surface can shift rather than shrink. An authenticated client with a valid certificate can send crafted XML that defines external entities, potentially causing the parser to read files on the server, trigger SSRF to internal services, or consume excessive resources. Because mTLS narrows access to a smaller set of trusted clients, developers may mistakenly assume the XML they receive is safe, leading to weaker controls around XML parsing. In practice, this means XXE becomes an authorization-path issue: the vulnerability exists inside an already authenticated and encrypted channel, which can make detection less obvious in logs focused primarily on transport security.
The combination also interacts with Rails’ own behavior. For example, if a Rails controller accepts XML via request.env["CONTENT_TYPE"] and passes the raw body to a parser without explicit safeguards, the server may follow external DTD references even when hosted behind mTLS-terminating infrastructure. Common triggers include using Nokogiri::XML without disabling DTD loading, or configuring an XML-RPC endpoint that does not set Nokogiri::XML::ParseOptions::NOENT and related safe options. An attacker who can present a valid client certificate might probe internal networks via parameterized external entities, turning mTLS from a boundary into a tunnel rather than a filter.
Because mTLS is often deployed in regulated or high-assurance environments, the impact of an XXE flaw can be more severe: sensitive configuration files, metadata service credentials, or internal service endpoints may be exposed. This is why scanning with a tool like middleBrick is valuable: it checks the unauthenticated attack surface where possible and, when combined with authenticated testing strategies, can surface XML parsing behavior that remains risky even when mTLS is used. Remediation focuses on parser configuration and input handling rather than transport-layer controls alone.
Mutual Tls-Specific Remediation in Rails — concrete code fixes
Securing Rails against XXE in an mTLS environment requires both transport configuration and safe XML parsing. On the mTLS side, Rails must be configured to require and validate client certificates without assuming safety inside the TLS channel. Below are concrete, working examples showing how to set up mTLS and how to parse XML safely.
mTLS setup in Rails
In config/application.rb or an environment-specific file such as config/environments/production.rb, enforce client certificate verification at the Rails level when using a reverse proxy or application-level termination. If termination happens at the load balancer or ingress, ensure it sets trusted headers only after validation and that Rails still enforces strict certificate checks for sensitive endpoints.
# config/initializers/ssl.rb — example for application-level mTLS enforcement
require 'openssl'
ssl_certificate = File.read('/path/to/ca_bundle.pem')
ssl_verify_mode = OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
Rails.application.configure do
config.middleware.use ActionDispatch::SSL,
hsts: true,
redirect: false,
ssl_certificate: ssl_certificate,
ssl_verify_mode: ssl_verify_mode
end
When using Puma directly with mTLS, configure the SSL context to require client authentication:
# 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.pem',
verify_mode: 'verify_peer'
}
Safe XML parsing in Rails
Always disable external entity processing when parsing XML. With Nokogiri, use the NOENT and NONET options together and avoid passing raw external subsets.
# app/services/safe_xml_parser.rb
require 'nokogiri'
module SafeXmlParser
def self.parse(xml_string)
# Disables DTD load, external entities, and network access during parsing
Nokogiri::XML(xml_string) do |config|
config.options = Nokogiri::XML::ParseOptions::NOENT | Nokogiri::XML::ParseOptions::NONET | Nokogiri::XML::ParseOptions::DTDLOAD
end
end
end
If your Rails app consumes XML-RPC, configure the underlying transport to reject external entities:
# app/services/xml_rpc_client.rb
require 'xmlrpc/client'
module XmlRpcClient
def self.safe_proxy(uri)
# Use a custom parser that disables dangerous features
parser = XMLRPC::Config::DEFAULT_PARSER.dup
parser.default_param = parser.default_param.map do |param|
if param.is_a?(XMLRPC::XMLParser::ExpatParser::Param)
# Ensure the parser enforces safe options
XMLRPC::XMLParser::ExpatParser::Param.new(
parser: parser,
options: XMLRPC::XMLParser::ExpatParser::Options::NOENT
)
else
param
end
end
XMLRPC::Client.new2(uri, parser: parser)
end
end
Combine these practices with runtime scanning via middleBrick to validate that your endpoints remain secure under both authenticated mTLS sessions and unexpected XML payloads. Continuous monitoring and CI/CD gates in the Pro plan can help catch regressions before they reach production.