Ruby XXE Prevention
Comprehensive guide to preventing XXE vulnerabilities in Ruby applications using REXML, Nokogiri, and other XML libraries.
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
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
56endNokogiri Vulnerable Configuration (DON'T DO THIS)
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
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}"
51endREXML Vulnerable Example
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
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
34endRails 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
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
60endSinatra Secure XML Endpoint
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
51endTesting XXE Prevention in Ruby
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
48endRuby 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 options
• config.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