Skip to content
🔵Info0.0

Ruby XXE Prevention

Comprehensive guide to preventing XXE vulnerabilities in Ruby applications using REXML, Nokogiri, and other XML libraries.

CWE-611: Improper Restriction of XML External Entity ReferenceOWASP Top 10:2021 - A05: Security Misconfiguration

Overview

Ruby has multiple XML parsing libraries with different security postures. Understanding each library's defaults and proper configuration is essential for preventing XXE vulnerabilities.

Common Ruby XML Libraries:

  • Nokogiri - Most popular, uses libxml2 (C library)
  • REXML - Pure Ruby, built-in to standard library
  • Ox - Fast XML parser
  • LibXML-Ruby - Direct libxml2 bindings

Security Status by Default:

  • Nokogiri 1.5.4+ - SAFE (noent disabled by default)
  • REXML - VULNERABLE (external entities enabled)
  • Ox - SAFE (doesn't support external entities)
  • LibXML-Ruby - VULNERABLE (depends on configuration)

Best Practice: Use Nokogiri with explicit security settings, or REXML with entity expansion disabled.

Nokogiri (Recommended - Safe by Default)

Nokogiri Security: Nokogiri 1.5.4+ is safe by default because it disables the noent option, preventing external entity expansion.

Default Behavior:

  • External entities are NOT expanded
  • DTDs are NOT loaded from external sources
  • Entity references are left as-is in the document

Verify Safe Configuration: Ensure you're not explicitly enabling dangerous options:

  • DON'T use Nokogiri::XML::ParseOptions::NOENT
  • DON'T use Nokogiri::XML::ParseOptions::DTDLOAD
  • DON'T use Nokogiri::XML::ParseOptions::DTDVALID

Safe Default Parse:

require 'nokogiri'

xml = Nokogiri::XML(xml_string)
# Safe: External entities not expanded

Nokogiri Secure Implementation

Rubynokogiri_secure.rb✓ Secure
1require 'nokogiri'
2
3# SECURE: Default parsing (safe by default in Nokogiri 1.5.4+)
4def parse_xml_secure(xml_string)
5  # This is safe - external entities not expanded
6  doc = Nokogiri::XML(xml_string)
7  doc
8end
9
10# SECURE: Explicit safe options
11def parse_xml_explicit_safe(xml_string)
12  # Explicitly set safe parse options
13  doc = Nokogiri::XML(xml_string) do |config|
14    # Strict parsing without entity expansion
15    config.strict.nonet
16  end
17  doc
18end
19
20# SECURE: Parse with validation but no entities
21def parse_xml_with_validation(xml_string)
22  doc = Nokogiri::XML(xml_string) do |config|
23    config.strict.nonet.noblanks
24  end
25  
26  # Validate against schema if needed
27  xsd = Nokogiri::XML::Schema(File.read('schema.xsd'))
28  errors = xsd.validate(doc)
29  
30  raise "Invalid XML: #{errors.join(', ')}" unless errors.empty?
31  
32  doc
33end
34
35# SECURE: Rails/Sinatra controller example
36class XMLController < ApplicationController
37  def parse
38    xml_string = request.body.read
39    
40    # Validate content type
41    unless request.content_type == 'application/xml'
42      render json: { error: 'Invalid content type' }, status: 400
43      return
44    end
45    
46    # Parse safely
47    doc = Nokogiri::XML(xml_string) do |config|
48      config.strict.nonet
49    end
50    
51    # Process document...
52    render json: { success: true }
53  rescue Nokogiri::XML::SyntaxError => e
54    render json: { error: 'Invalid XML' }, status: 400
55  end
56end

Nokogiri Vulnerable Configuration (DON'T DO THIS)

Rubynokogiri_vulnerable.rb⚠️ Vulnerable
1require 'nokogiri'
2
3# VULNERABLE: Explicitly enabling dangerous options
4def parse_xml_vulnerable(xml_string)
5  # DON'T DO THIS! Enables entity expansion
6  doc = Nokogiri::XML(xml_string) do |config|
7    config.options = Nokogiri::XML::ParseOptions::NOENT |
8                     Nokogiri::XML::ParseOptions::DTDLOAD
9  end
10  doc
11end
12
13# VULNERABLE: Using NOENT flag
14def parse_with_noent(xml_string)
15  # NOENT expands entities - DANGEROUS!
16  doc = Nokogiri::XML(xml_string, nil, nil, Nokogiri::XML::ParseOptions::NOENT)
17  doc
18end
19
20# VULNERABLE: Old Nokogiri versions
21# Nokogiri < 1.5.4 is vulnerable by default
22# Upgrade to 1.5.4 or later!
23
24# Attack payload that would work with vulnerable configuration:
25# <?xml version="1.0"?>
26# <!DOCTYPE root [
27#   <!ENTITY xxe SYSTEM "file:///etc/passwd">
28# ]>
29# <root>&xxe;</root>

REXML (Vulnerable by Default)

REXML Security Warning: REXML is Ruby's built-in XML parser but is VULNERABLE to XXE by default. External entities are expanded unless explicitly disabled.

Default Behavior:

  • External entities ARE expanded
  • Can read local files
  • Can make HTTP requests
  • Vulnerable to billion laughs DoS

If You Must Use REXML:

  • Set custom entity expansion limit
  • Disable DOCTYPE declarations if not needed
  • Validate input before parsing
  • Consider switching to Nokogiri

Better Alternative: Use Nokogiri instead of REXML for better security defaults and performance.

REXML Secure Implementation

Rubyrexml_secure.rb✓ Secure
1require 'rexml/document'
2require 'rexml/entity'
3
4# SECURE: Disable entity expansion in REXML
5class SecureREXMLParser
6  def self.parse(xml_string)
7    # Reject if contains DOCTYPE (strictest approach)
8    if xml_string =~ /<!DOCTYPE/i
9      raise SecurityError, "DOCTYPE not allowed"
10    end
11    
12    # Create document
13    doc = REXML::Document.new(xml_string)
14    
15    # Verify no entities were expanded
16    check_for_entities(doc)
17    
18    doc
19  end
20  
21  # SECURE: Alternative - limit entity expansion
22  def self.parse_with_limit(xml_string)
23    # Set entity expansion limit (prevents billion laughs)
24    REXML::Security.entity_expansion_limit = 1000
25    REXML::Security.entity_expansion_text_limit = 10240
26    
27    doc = REXML::Document.new(xml_string)
28    doc
29  end
30  
31  private
32  
33  def self.check_for_entities(doc)
34    # Check for entity references in document
35    doc.each_element do |element|
36      element.attributes.each do |name, value|
37        if value.to_s.include?('&')
38          raise SecurityError, "Entity reference detected"
39        end
40      end
41    end
42  end
43end
44
45# Usage:
46begin
47  doc = SecureREXMLParser.parse(xml_string)
48  # Process document...
49rescue SecurityError => e
50  puts "Security error: #{e.message}"
51end

REXML Vulnerable Example

Rubyrexml_vulnerable.rb⚠️ Vulnerable
1require 'rexml/document'
2
3# VULNERABLE: Default REXML parsing
4def parse_xml(xml_string)
5  # This is VULNERABLE! External entities will be expanded
6  doc = REXML::Document.new(xml_string)
7  doc
8end
9
10# Example vulnerable usage:
11xml = <<-XML
12<?xml version="1.0"?>
13<!DOCTYPE root [
14  <!ENTITY xxe SYSTEM "file:///etc/passwd">
15]>
16<root>
17  <data>&xxe;</data>
18</root>
19XML
20
21doc = parse_xml(xml)
22# The &xxe; entity will be expanded to /etc/passwd contents!
23puts doc.elements['root/data'].text
24# Output: Contents of /etc/passwd
25
26# VULNERABLE: Even with entity expansion limits, still risky
27REXML::Security.entity_expansion_limit = 100
28doc = REXML::Document.new(xml_string)
29# Still vulnerable to file disclosure and SSRF!

Ox Parser (Safe - No External Entity Support)

Ox Security: Ox is a fast XML parser that does NOT support external entities, making it inherently safe from XXE.

Security Features:

  • No external entity support
  • No DTD processing
  • Fast performance
  • Simple API

Limitations:

  • Cannot validate against DTDs
  • Limited DOM manipulation
  • Less feature-rich than Nokogiri

Use Case: Good choice for parsing untrusted XML when you don't need DTD validation and want guaranteed XXE safety.

Ox Secure Usage

Rubyox_secure.rb✓ Secure
1require 'ox'
2
3# SECURE: Ox doesn't support external entities
4def parse_with_ox(xml_string)
5  # Safe - Ox doesn't process external entities
6  doc = Ox.parse(xml_string)
7  doc
8end
9
10# Example usage:
11xml = <<-XML
12<?xml version="1.0"?>
13<!DOCTYPE root [
14  <!ENTITY xxe SYSTEM "file:///etc/passwd">
15]>
16<root>
17  <data>&xxe;</data>
18</root>
19XML
20
21doc = parse_with_ox(xml)
22# Ox will NOT expand the &xxe; entity
23# The entity reference will be ignored or left as-is
24
25# Additional Ox features
26class XMLHandler
27  def self.parse_safe(xml_string)
28    # Ox options for strict parsing
29    doc = Ox.parse(xml_string, mode: :hash, symbolize_keys: false)
30    doc
31  rescue Ox::ParseError => e
32    { error: 'Invalid XML format' }
33  end
34end

Rails XML Parsing Security

Rails XML Parameter Parsing: Rails automatically parses XML request bodies. Ensure secure configuration.

Rails 6.1+:

  • Uses Nokogiri by default (safe)
  • External entities not expanded
  • No additional configuration needed

Rails < 6.1:

  • May use REXML or older Nokogiri
  • Verify Nokogiri version >= 1.5.4
  • Consider disabling XML parameter parsing if not needed

Disable XML Parsing (if not needed):

# config/initializers/disable_xml.rb
ActionDispatch::ParamsParser::DEFAULT_PARSERS.delete(Mime[:xml])

Explicit Safe Parsing:

# Use Nokogiri explicitly in controllers
class APIController < ApplicationController
  def parse_xml
    xml_doc = Nokogiri::XML(request.body.read) do |config|
      config.strict.nonet
    end
    # Process...
  end
end

Rails Secure XML Controller

Rubyxml_controller.rb✓ Secure
1# app/controllers/xml_controller.rb
2class XMLController < ApplicationController
3  # Skip CSRF for API endpoints (use authentication instead)
4  skip_before_action :verify_authenticity_token
5  
6  before_action :validate_content_type
7  before_action :validate_xml_size
8  
9  def create
10    xml_string = request.body.read
11    
12    # Parse securely with Nokogiri
13    doc = parse_xml_secure(xml_string)
14    
15    # Extract data from XML
16    data = extract_data(doc)
17    
18    # Process data...
19    render json: { success: true, data: data }
20  rescue Nokogiri::XML::SyntaxError => e
21    render json: { error: 'Invalid XML syntax' }, status: 400
22  rescue SecurityError => e
23    render json: { error: 'Security violation detected' }, status: 403
24  end
25  
26  private
27  
28  def validate_content_type
29    unless request.content_type == 'application/xml'
30      render json: { error: 'Invalid content type' }, status: 415
31    end
32  end
33  
34  def validate_xml_size
35    max_size = 1.megabyte
36    if request.body.size > max_size
37      render json: { error: 'XML too large' }, status: 413
38    end
39  end
40  
41  def parse_xml_secure(xml_string)
42    # Reject DOCTYPE if not needed
43    if xml_string =~ /<!DOCTYPE/i
44      raise SecurityError, 'DOCTYPE not allowed'
45    end
46    
47    # Parse with strict, safe options
48    Nokogiri::XML(xml_string) do |config|
49      config.strict.nonet.noblanks
50    end
51  end
52  
53  def extract_data(doc)
54    # Safely extract data from parsed XML
55    {
56      name: doc.at_xpath('//name')&.text,
57      value: doc.at_xpath('//value')&.text
58    }
59  end
60end

Sinatra Secure XML Endpoint

Rubysinatra_xml_app.rb✓ Secure
1require 'sinatra'
2require 'nokogiri'
3require 'json'
4
5# Sinatra app with secure XML parsing
6class XMLApp < Sinatra::Base
7  configure do
8    set :max_xml_size, 1_048_576 # 1MB
9  end
10  
11  post '/api/xml' do
12    content_type :json
13    
14    # Validate content type
15    halt 415, { error: 'Invalid content type' }.to_json unless
16      request.content_type == 'application/xml'
17    
18    # Read and validate size
19    xml_string = request.body.read
20    halt 413, { error: 'XML too large' }.to_json if
21      xml_string.bytesize > settings.max_xml_size
22    
23    # Reject DOCTYPE
24    halt 403, { error: 'DOCTYPE not allowed' }.to_json if
25      xml_string =~ /<!DOCTYPE/i
26    
27    begin
28      # Parse securely
29      doc = Nokogiri::XML(xml_string) do |config|
30        config.strict.nonet.noblanks
31      end
32      
33      # Process document
34      result = process_xml(doc)
35      
36      { success: true, data: result }.to_json
37    rescue Nokogiri::XML::SyntaxError => e
38      halt 400, { error: 'Invalid XML syntax' }.to_json
39    end
40  end
41  
42  private
43  
44  def process_xml(doc)
45    # Extract and return data
46    {
47      root: doc.root.name,
48      elements: doc.root.elements.count
49    }
50  end
51end

Testing XXE Prevention in Ruby

Rubyxxe_prevention_spec.rb✓ Secure
1# spec/security/xxe_prevention_spec.rb
2require 'rails_helper'
3
4RSpec.describe 'XXE Prevention', type: :request do
5  describe 'POST /api/xml' do
6    let(:xxe_payload) do
7      <<~XML
8        <?xml version="1.0"?>
9        <!DOCTYPE root [
10          <!ENTITY xxe SYSTEM "file:///etc/passwd">
11        ]>
12        <root>
13          <data>&xxe;</data>
14        </root>
15      XML
16    end
17    
18    it 'rejects DOCTYPE declarations' do
19      post '/api/xml',
20           params: xxe_payload,
21           headers: { 'CONTENT_TYPE' => 'application/xml' }
22      
23      expect(response).to have_http_status(:forbidden)
24      expect(JSON.parse(response.body)['error']).to eq('DOCTYPE not allowed')
25    end
26    
27    it 'does not expand external entities with Nokogiri' do
28      # Even if DOCTYPE allowed, entities should not expand
29      xml = '<?xml version="1.0"?><root><data>safe</data></root>'
30      
31      post '/api/xml',
32           params: xml,
33           headers: { 'CONTENT_TYPE' => 'application/xml' }
34      
35      expect(response).to have_http_status(:success)
36    end
37    
38    it 'rejects oversized XML' do
39      large_xml = '<root>' + 'a' * 2_000_000 + '</root>'
40      
41      post '/api/xml',
42           params: large_xml,
43           headers: { 'CONTENT_TYPE' => 'application/xml' }
44      
45      expect(response).to have_http_status(413)
46    end
47  end
48end

Ruby XXE Prevention Checklist

Use Nokogiri 1.5.4 or later • Verify: nokogiri --version • Update if needed: gem update nokogiri

Avoid REXML for untrusted input • If must use REXML, disable DOCTYPE • Set entity expansion limits • Validate input before parsing

Never use NOENT or DTDLOAD options • Don't enable entity expansion • Don't load external DTDs

Validate input before parsing • Check content type • Reject if contains DOCTYPE (if not needed) • Enforce size limits

Use strict parsing optionsconfig.strict.nonet • Catch Nokogiri::XML::SyntaxError

Test XXE prevention • Include security tests in test suite • Test with actual XXE payloads • Verify entities not expanded

Rails-specific • Verify Nokogiri version in Gemfile.lock • Consider disabling XML parsing if not needed • Use explicit parsing in controllers

Dependencies • Run bundle audit regularly • Keep all gems updated • Monitor security advisories