mirror of
https://github.com/ezyang/htmlpurifier.git
synced 2024-11-09 15:28:40 +00:00
Fix Internet Explorer innerHTML bug.
Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
This commit is contained in:
parent
94ed3b1231
commit
0dd9e4faf4
5
NEWS
5
NEWS
@ -14,6 +14,11 @@ NEWS ( CHANGELOG and HISTORY ) HTMLPurifier
|
|||||||
API change. The old API still works but will emit a warning,
|
API change. The old API still works but will emit a warning,
|
||||||
see http://htmlpurifier.org/docs/enduser-customize.html#optimized
|
see http://htmlpurifier.org/docs/enduser-customize.html#optimized
|
||||||
for how to upgrade your code.
|
for how to upgrade your code.
|
||||||
|
# Protect against Internet Explorer innerHTML behavior by specially
|
||||||
|
treating attributes with backticks but no angled brackets, quotes or
|
||||||
|
spaces. This constitutes a slight semantic change, which can be
|
||||||
|
reverted using %Output.FixInnerHTML. Reported by Neike Taika-Tessaro
|
||||||
|
and Mario Heiderich.
|
||||||
! Added %HTML.Nofollow to add rel="nofollow" to external links.
|
! Added %HTML.Nofollow to add rel="nofollow" to external links.
|
||||||
! More types of SPL autoloaders allowed on later versions of PHP.
|
! More types of SPL autoloaders allowed on later versions of PHP.
|
||||||
! Implementations for position, top, left, right, bottom, z-index
|
! Implementations for position, top, left, right, bottom, z-index
|
||||||
|
Binary file not shown.
@ -0,0 +1,15 @@
|
|||||||
|
Output.FixInnerHTML
|
||||||
|
TYPE: bool
|
||||||
|
VERSION: 4.3.0
|
||||||
|
DEFAULT: true
|
||||||
|
--DESCRIPTION--
|
||||||
|
<p>
|
||||||
|
If true, HTML Purifier will protect against Internet Explorer's
|
||||||
|
mishandling of the <code>innerHTML</code> attribute by appending
|
||||||
|
a space to any attribute that does not contain angled brackets, spaces
|
||||||
|
or quotes, but contains a backtick. This slightly changes the
|
||||||
|
semantics of any given attribute, so if this is unacceptable and
|
||||||
|
you do not use <code>innerHTML</code> on any of your pages, you can
|
||||||
|
turn this directive off.
|
||||||
|
</p>
|
||||||
|
--# vim: et sw=4 sts=4
|
@ -36,6 +36,11 @@ class HTMLPurifier_Generator
|
|||||||
*/
|
*/
|
||||||
private $_flashCompat;
|
private $_flashCompat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of %Output.FixInnerHTML
|
||||||
|
*/
|
||||||
|
private $_innerHTMLFix;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stack for keeping track of object information when outputting IE
|
* Stack for keeping track of object information when outputting IE
|
||||||
* compatibility code.
|
* compatibility code.
|
||||||
@ -54,6 +59,7 @@ class HTMLPurifier_Generator
|
|||||||
public function __construct($config, $context) {
|
public function __construct($config, $context) {
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
$this->_scriptFix = $config->get('Output.CommentScriptContents');
|
$this->_scriptFix = $config->get('Output.CommentScriptContents');
|
||||||
|
$this->_innerHTMLFix = $config->get('Output.FixInnerHTML');
|
||||||
$this->_sortAttr = $config->get('Output.SortAttr');
|
$this->_sortAttr = $config->get('Output.SortAttr');
|
||||||
$this->_flashCompat = $config->get('Output.FlashCompat');
|
$this->_flashCompat = $config->get('Output.FlashCompat');
|
||||||
$this->_def = $config->getHTMLDefinition();
|
$this->_def = $config->getHTMLDefinition();
|
||||||
@ -190,6 +196,37 @@ class HTMLPurifier_Generator
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Workaround for Internet Explorer innerHTML bug.
|
||||||
|
// Essentially, Internet Explorer, when calculating
|
||||||
|
// innerHTML, omits quotes if there are no instances of
|
||||||
|
// angled brackets, quotes or spaces. However, when parsing
|
||||||
|
// HTML (for example, when you assign to innerHTML), it
|
||||||
|
// treats backticks as quotes. Thus,
|
||||||
|
// <img alt="``" />
|
||||||
|
// becomes
|
||||||
|
// <img alt=`` />
|
||||||
|
// becomes
|
||||||
|
// <img alt='' />
|
||||||
|
// Fortunately, all we need to do is trigger an appropriate
|
||||||
|
// quoting style, which we do by adding an extra space.
|
||||||
|
// This also is consistent with the W3C spec, which states
|
||||||
|
// that user agents may ignore leading or trailing
|
||||||
|
// whitespace (in fact, most don't, at least for attributes
|
||||||
|
// like alt, but an extra space at the end is barely
|
||||||
|
// noticeable). Still, we have a configuration knob for
|
||||||
|
// this, since this transformation is not necesary if you
|
||||||
|
// don't process user input with innerHTML or you don't plan
|
||||||
|
// on supporting Internet Explorer.
|
||||||
|
if ($this->_innerHTMLFix) {
|
||||||
|
if (strpos($value, '`') !== false) {
|
||||||
|
// check if correct quoting style would not already be
|
||||||
|
// triggered
|
||||||
|
if (strcspn($value, '"\' <>') === strlen($value)) {
|
||||||
|
// protect!
|
||||||
|
$value .= ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
$html .= $key.'="'.$this->escape($value).'" ';
|
$html .= $key.'="'.$this->escape($value).'" ';
|
||||||
}
|
}
|
||||||
return rtrim($html);
|
return rtrim($html);
|
||||||
|
33
smoketests/innerHTML.html
Normal file
33
smoketests/innerHTML.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>innerHTML smoketest</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
What we're going to do is use JavaScript to calculate
|
||||||
|
fixpoints of innerHTML parse and reparsing. We start with
|
||||||
|
an input value, encoded in a JavaScript string.
|
||||||
|
|
||||||
|
x.innerHTML = input
|
||||||
|
|
||||||
|
We then snapshot the DOM state of x, and then perform the
|
||||||
|
iteration:
|
||||||
|
|
||||||
|
intermediate = x.innerHTML
|
||||||
|
x.innerHTML = intermediate
|
||||||
|
|
||||||
|
What inputs are we going to test?
|
||||||
|
|
||||||
|
We will generate using the following alphabet:
|
||||||
|
|
||||||
|
a01~!@#$%^&*()_+`-=[]\{}|;':",./<>? (and <space>)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-->
|
||||||
|
<textarea id="out" style="width:100%;height:100%;"></textarea>
|
||||||
|
<div id="testContainer" style="display:none"></div>
|
||||||
|
<script src="innerHTML.js" type="text/javascript"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
51
smoketests/innerHTML.js
Normal file
51
smoketests/innerHTML.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
var alphabet = 'a!`=[]\\;\':"/<> &';
|
||||||
|
|
||||||
|
var out = document.getElementById('out');
|
||||||
|
var testContainer = document.getElementById('testContainer');
|
||||||
|
|
||||||
|
function print(s) {
|
||||||
|
out.value += s + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function testImage() {
|
||||||
|
return testContainer.firstChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test(input) {
|
||||||
|
var count = 0;
|
||||||
|
var oldInput, newInput;
|
||||||
|
testContainer.innerHTML = "<img />";
|
||||||
|
testImage().setAttribute("alt", input);
|
||||||
|
print("------");
|
||||||
|
print("Test input: " + input);
|
||||||
|
do {
|
||||||
|
oldInput = testImage().getAttribute("alt");
|
||||||
|
var intermediate = testContainer.innerHTML;
|
||||||
|
print("Render: " + intermediate);
|
||||||
|
testContainer.innerHTML = intermediate;
|
||||||
|
if (testImage() == null) {
|
||||||
|
print("Image disappeared...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
newInput = testImage().getAttribute("alt");
|
||||||
|
print("New value: " + newInput);
|
||||||
|
count++;
|
||||||
|
} while (count < 5 && newInput != oldInput);
|
||||||
|
if (count == 5) {
|
||||||
|
print("Failed to achieve fixpoint");
|
||||||
|
}
|
||||||
|
testContainer.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Go!");
|
||||||
|
|
||||||
|
test("`` ");
|
||||||
|
test("'' ");
|
||||||
|
|
||||||
|
for (var i = 0; i < alphabet.length; i++) {
|
||||||
|
for (var j = 0; j < alphabet.length; j++) {
|
||||||
|
test(alphabet.charAt(i) + alphabet.charAt(j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// document.getElementById('out').textContent = alphabet;
|
@ -82,7 +82,7 @@ class HTMLPurifier_GeneratorTest extends HTMLPurifier_Harness
|
|||||||
$this->assertGenerateFromToken( null, '' );
|
$this->assertGenerateFromToken( null, '' );
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_generateFromToken_() {
|
function test_generateFromToken_unicode() {
|
||||||
$theta_char = $this->_entity_lookup->table['theta'];
|
$theta_char = $this->_entity_lookup->table['theta'];
|
||||||
$this->assertGenerateFromToken(
|
$this->assertGenerateFromToken(
|
||||||
new HTMLPurifier_Token_Text($theta_char),
|
new HTMLPurifier_Token_Text($theta_char),
|
||||||
@ -90,6 +90,28 @@ class HTMLPurifier_GeneratorTest extends HTMLPurifier_Harness
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function test_generateFromToken_backtick() {
|
||||||
|
$this->assertGenerateFromToken(
|
||||||
|
new HTMLPurifier_Token_Start('img', array('alt' => '`foo')),
|
||||||
|
'<img alt="`foo ">'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_generateFromToken_backtickDisabled() {
|
||||||
|
$this->config->set('Output.FixInnerHTML', false);
|
||||||
|
$this->assertGenerateFromToken(
|
||||||
|
new HTMLPurifier_Token_Start('img', array('alt' => '`')),
|
||||||
|
'<img alt="`">'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_generateFromToken_backtickNoChange() {
|
||||||
|
$this->assertGenerateFromToken(
|
||||||
|
new HTMLPurifier_Token_Start('img', array('alt' => '`foo` bar')),
|
||||||
|
'<img alt="`foo` bar">'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function assertGenerateAttributes($attr, $expect, $element = false) {
|
function assertGenerateAttributes($attr, $expect, $element = false) {
|
||||||
$generator = $this->createGenerator();
|
$generator = $this->createGenerator();
|
||||||
$result = $generator->generateAttributes($attr, $element);
|
$result = $generator->generateAttributes($attr, $element);
|
||||||
|
Loading…
Reference in New Issue
Block a user