Skip to content

Commit 033d190

Browse files
authored
Add support for XML entity expansion limitation in SAX and pull parsers (#187)
- Supported `REXML::Security.entity_expansion_limit=` in SAX and pull parsers - Supported `REXML::Security.entity_expansion_text_limit=` in SAX and pull parsers
1 parent 086287c commit 033d190

File tree

6 files changed

+222
-12
lines changed

6 files changed

+222
-12
lines changed

lib/rexml/parsers/baseparser.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,15 @@ def initialize( source )
154154
self.stream = source
155155
@listeners = []
156156
@prefixes = Set.new
157+
@entity_expansion_count = 0
157158
end
158159

159160
def add_listener( listener )
160161
@listeners << listener
161162
end
162163

163164
attr_reader :source
165+
attr_reader :entity_expansion_count
164166

165167
def stream=( source )
166168
@source = SourceFactory.create_from( source )
@@ -513,7 +515,9 @@ def pull_event
513515
def entity( reference, entities )
514516
value = nil
515517
value = entities[ reference ] if entities
516-
if not value
518+
if value
519+
record_entity_expansion
520+
else
517521
value = DEFAULT_ENTITIES[ reference ]
518522
value = value[2] if value
519523
end
@@ -552,12 +556,17 @@ def unnormalize( string, entities=nil, filter=nil )
552556
}
553557
matches.collect!{|x|x[0]}.compact!
554558
if matches.size > 0
559+
sum = 0
555560
matches.each do |entity_reference|
556561
unless filter and filter.include?(entity_reference)
557562
entity_value = entity( entity_reference, entities )
558563
if entity_value
559564
re = Private::DEFAULT_ENTITIES_PATTERNS[entity_reference] || /&#{entity_reference};/
560565
rv.gsub!( re, entity_value )
566+
sum += rv.bytesize
567+
if sum > Security.entity_expansion_text_limit
568+
raise "entity expansion has grown too large"
569+
end
561570
else
562571
er = DEFAULT_ENTITIES[entity_reference]
563572
rv.gsub!( er[0], er[2] ) if er
@@ -570,6 +579,14 @@ def unnormalize( string, entities=nil, filter=nil )
570579
end
571580

572581
private
582+
583+
def record_entity_expansion
584+
@entity_expansion_count += 1
585+
if @entity_expansion_count > Security.entity_expansion_limit
586+
raise "number of entity expansions exceeded, processing aborted."
587+
end
588+
end
589+
573590
def need_source_encoding_update?(xml_declaration_encoding)
574591
return false if xml_declaration_encoding.nil?
575592
return false if /\AUTF-16\z/i =~ xml_declaration_encoding

lib/rexml/parsers/pullparser.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def add_listener( listener )
4747
@listeners << listener
4848
end
4949

50+
def entity_expansion_count
51+
@parser.entity_expansion_count
52+
end
53+
5054
def each
5155
while has_next?
5256
yield self.pull

lib/rexml/parsers/sax2parser.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ def source
2222
@parser.source
2323
end
2424

25+
def entity_expansion_count
26+
@parser.entity_expansion_count
27+
end
28+
2529
def add_listener( listener )
2630
@parser.add_listener( listener )
2731
end

test/test_document.rb

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def teardown
4141

4242
class GeneralEntityTest < self
4343
def test_have_value
44-
xml = <<EOF
44+
xml = <<XML
4545
<?xml version="1.0" encoding="UTF-8"?>
4646
<!DOCTYPE member [
4747
<!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
@@ -55,23 +55,24 @@ def test_have_value
5555
<member>
5656
&a;
5757
</member>
58-
EOF
58+
XML
5959

6060
doc = REXML::Document.new(xml)
61-
assert_raise(RuntimeError) do
61+
assert_raise(RuntimeError.new("entity expansion has grown too large")) do
6262
doc.root.children.first.value
6363
end
64+
6465
REXML::Security.entity_expansion_limit = 100
6566
assert_equal(100, REXML::Security.entity_expansion_limit)
6667
doc = REXML::Document.new(xml)
67-
assert_raise(RuntimeError) do
68+
assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
6869
doc.root.children.first.value
6970
end
7071
assert_equal(101, doc.entity_expansion_count)
7172
end
7273

7374
def test_empty_value
74-
xml = <<EOF
75+
xml = <<XML
7576
<?xml version="1.0" encoding="UTF-8"?>
7677
<!DOCTYPE member [
7778
<!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
@@ -85,23 +86,24 @@ def test_empty_value
8586
<member>
8687
&a;
8788
</member>
88-
EOF
89+
XML
8990

9091
doc = REXML::Document.new(xml)
91-
assert_raise(RuntimeError) do
92+
assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
9293
doc.root.children.first.value
9394
end
95+
9496
REXML::Security.entity_expansion_limit = 100
9597
assert_equal(100, REXML::Security.entity_expansion_limit)
9698
doc = REXML::Document.new(xml)
97-
assert_raise(RuntimeError) do
99+
assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
98100
doc.root.children.first.value
99101
end
100102
assert_equal(101, doc.entity_expansion_count)
101103
end
102104

103105
def test_with_default_entity
104-
xml = <<EOF
106+
xml = <<XML
105107
<?xml version="1.0" encoding="UTF-8"?>
106108
<!DOCTYPE member [
107109
<!ENTITY a "a">
@@ -112,14 +114,15 @@ def test_with_default_entity
112114
&a2;
113115
&lt;
114116
</member>
115-
EOF
117+
XML
116118

117119
REXML::Security.entity_expansion_limit = 4
118120
doc = REXML::Document.new(xml)
119121
assert_equal("\na\na a\n<\n", doc.root.children.first.value)
122+
120123
REXML::Security.entity_expansion_limit = 3
121124
doc = REXML::Document.new(xml)
122-
assert_raise(RuntimeError) do
125+
assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
123126
doc.root.children.first.value
124127
end
125128
end

test/test_pullparser.rb

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,5 +155,101 @@ def test_peek
155155
end
156156
assert_equal( 0, names.length )
157157
end
158+
159+
class EntityExpansionLimitTest < Test::Unit::TestCase
160+
def setup
161+
@default_entity_expansion_limit = REXML::Security.entity_expansion_limit
162+
end
163+
164+
def teardown
165+
REXML::Security.entity_expansion_limit = @default_entity_expansion_limit
166+
end
167+
168+
class GeneralEntityTest < self
169+
def test_have_value
170+
source = <<-XML
171+
<?xml version="1.0" encoding="UTF-8"?>
172+
<!DOCTYPE member [
173+
<!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
174+
<!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
175+
<!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
176+
<!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
177+
<!ENTITY e "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
178+
]>
179+
<member>
180+
&a;
181+
</member>
182+
XML
183+
184+
parser = REXML::Parsers::PullParser.new(source)
185+
assert_raise(RuntimeError.new("entity expansion has grown too large")) do
186+
while parser.has_next?
187+
parser.pull
188+
end
189+
end
190+
end
191+
192+
def test_empty_value
193+
source = <<-XML
194+
<?xml version="1.0" encoding="UTF-8"?>
195+
<!DOCTYPE member [
196+
<!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
197+
<!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
198+
<!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
199+
<!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
200+
<!ENTITY e "">
201+
]>
202+
<member>
203+
&a;
204+
</member>
205+
XML
206+
207+
parser = REXML::Parsers::PullParser.new(source)
208+
assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
209+
while parser.has_next?
210+
parser.pull
211+
end
212+
end
213+
214+
REXML::Security.entity_expansion_limit = 100
215+
parser = REXML::Parsers::PullParser.new(source)
216+
assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
217+
while parser.has_next?
218+
parser.pull
219+
end
220+
end
221+
assert_equal(101, parser.entity_expansion_count)
222+
end
223+
224+
def test_with_default_entity
225+
source = <<-XML
226+
<?xml version="1.0" encoding="UTF-8"?>
227+
<!DOCTYPE member [
228+
<!ENTITY a "a">
229+
<!ENTITY a2 "&a; &a;">
230+
]>
231+
<member>
232+
&a;
233+
&a2;
234+
&lt;
235+
</member>
236+
XML
237+
238+
REXML::Security.entity_expansion_limit = 4
239+
parser = REXML::Parsers::PullParser.new(source)
240+
while parser.has_next?
241+
parser.pull
242+
end
243+
244+
REXML::Security.entity_expansion_limit = 3
245+
parser = REXML::Parsers::PullParser.new(source)
246+
assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
247+
while parser.has_next?
248+
parser.pull
249+
end
250+
end
251+
end
252+
end
253+
end
158254
end
159255
end

test/test_sax.rb

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,92 @@ def test_sax2
9999
end
100100
end
101101

102+
class EntityExpansionLimitTest < Test::Unit::TestCase
103+
def setup
104+
@default_entity_expansion_limit = REXML::Security.entity_expansion_limit
105+
end
106+
107+
def teardown
108+
REXML::Security.entity_expansion_limit = @default_entity_expansion_limit
109+
end
110+
111+
class GeneralEntityTest < self
112+
def test_have_value
113+
source = <<-XML
114+
<?xml version="1.0" encoding="UTF-8"?>
115+
<!DOCTYPE member [
116+
<!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
117+
<!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
118+
<!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
119+
<!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
120+
<!ENTITY e "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
121+
]>
122+
<member>
123+
&a;
124+
</member>
125+
XML
126+
127+
sax = REXML::Parsers::SAX2Parser.new(source)
128+
assert_raise(RuntimeError.new("entity expansion has grown too large")) do
129+
sax.parse
130+
end
131+
end
132+
133+
def test_empty_value
134+
source = <<-XML
135+
<?xml version="1.0" encoding="UTF-8"?>
136+
<!DOCTYPE member [
137+
<!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
138+
<!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
139+
<!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
140+
<!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
141+
<!ENTITY e "">
142+
]>
143+
<member>
144+
&a;
145+
</member>
146+
XML
147+
148+
sax = REXML::Parsers::SAX2Parser.new(source)
149+
assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
150+
sax.parse
151+
end
152+
153+
REXML::Security.entity_expansion_limit = 100
154+
sax = REXML::Parsers::SAX2Parser.new(source)
155+
assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
156+
sax.parse
157+
end
158+
assert_equal(101, sax.entity_expansion_count)
159+
end
160+
161+
def test_with_default_entity
162+
source = <<-XML
163+
<?xml version="1.0" encoding="UTF-8"?>
164+
<!DOCTYPE member [
165+
<!ENTITY a "a">
166+
<!ENTITY a2 "&a; &a;">
167+
]>
168+
<member>
169+
&a;
170+
&a2;
171+
&lt;
172+
</member>
173+
XML
174+
175+
REXML::Security.entity_expansion_limit = 4
176+
sax = REXML::Parsers::SAX2Parser.new(source)
177+
sax.parse
178+
179+
REXML::Security.entity_expansion_limit = 3
180+
sax = REXML::Parsers::SAX2Parser.new(source)
181+
assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
182+
sax.parse
183+
end
184+
end
185+
end
186+
end
187+
102188
# used by test_simple_doctype_listener
103189
# submitted by Jeff Barczewski
104190
class SimpleDoctypeListener

0 commit comments

Comments
 (0)