diff --git a/lib/HTMLParser.js b/lib/HTMLParser.js index ef40ee6..189108b 100644 --- a/lib/HTMLParser.js +++ b/lib/HTMLParser.js @@ -1973,6 +1973,18 @@ function HTMLParser(address, fragmentContext, options) { var templateInsertionModes = []; // Stack of template insertion modes. var stack = new HTMLParser.ElementStack(); // Stack of open elements var afe = new HTMLParser.ActiveFormattingElements(); // mis-nested tags + + // Reconstructing stale active formatting elements can otherwise turn a + // small input into a quadratic number of DOM allocations. Keep the total + // reconstruction work linear in the amount of input supplied to this + // parser. The floor avoids affecting small documents with ordinary HTML + // error recovery, while the per-character allowance scales for large + // documents. + var input_char_count = 0; + var afe_reconstruction_count = 0; + var MIN_AFE_RECONSTRUCTION_BUDGET = 4096; + var AFE_RECONSTRUCTIONS_PER_INPUT_CHAR = 2; + var fragment = (fragmentContext!==undefined); // For innerHTML, etc. var head_element_pointer = null; var form_element_pointer = null; @@ -2040,6 +2052,11 @@ function HTMLParser(address, fragmentContext, options) { parse: function(s, end, shouldPauseFunc) { var moreToDo; + // Count only newly supplied input. Buffered leftovers are already + // accounted for, while process() and resume() re-enter with an empty + // string and therefore do not count the same input twice. + input_char_count += s.length; + // If we're paused, remember the text to parse, but // don't parse it now. // (Don't invoke shouldPauseFunc because we haven't handled 'end' yet.) @@ -2850,6 +2867,15 @@ function HTMLParser(address, fragmentContext, options) { // one, recreating formatting elements and pushing them back onto // the list of open elements for(i = i+1; i < afe.list.length; i++) { + afe_reconstruction_count++; + var reconstruction_budget = Math.max( + MIN_AFE_RECONSTRUCTION_BUDGET, + input_char_count * AFE_RECONSTRUCTIONS_PER_INPUT_CHAR + ); + if (afe_reconstruction_count > reconstruction_budget) { + throw new Error('HTML parser active formatting reconstruction limit exceeded'); + } + var newelt = insertElement(function(doc) { return afeclone(doc, i).elt; }); afe.list[i] = newelt; } diff --git a/test/parsing.js b/test/parsing.js index 167efd4..d46345c 100644 --- a/test/parsing.js +++ b/test/parsing.js @@ -260,3 +260,34 @@ exports.parseAlgorithm.activeFormattingElements = { } } }; + + +exports.parseAlgorithm['active formatting reconstruction budget'] = { + 'bounds pathological fragment reconstruction': function() { + var doc = domino.createDocument(); + var template = doc.createElement('template'); + var html = ''; + + for (var i = 0; i < 100; i++) { + html += '

'; + } + + assert.throws(function() { + template.innerHTML = html; + }, /active formatting reconstruction limit exceeded/); + }, + + 'allows normally nested formatting elements': function() { + var doc = domino.createDocument(); + var template = doc.createElement('template'); + var html = ''; + + for (var i = 0; i < 100; i++) { + html += '

'; + } + + assert.doesNotThrow(function() { + template.innerHTML = html; + }); + } +};