在JavaScript中如何取消转义HTML实体?

293

我有一些JavaScript代码,用它来与XML-RPC后端进行通信。 XML-RPC返回以下格式的字符串:

<img src='myimage.jpg'>

然而,当我使用 JavaScript 将字符串插入 HTML 时,它们会直接渲染为字符串。我看不到图像,我只看到字面的字符串:

<img src='myimage.jpg'>

我猜测HTML在XML-RPC通道上被转义了。

我该如何在JavaScript中取消转义字符串?我尝试了这个页面上的技术,但没有成功:http://paulschreiber.com/blog/2008/09/20/javascript-how-to-unescape-html-entities/

还有哪些方法可以诊断问题?


这篇文章中包含的庞大函数似乎运行良好:http://blogs.msdn.com/b/aoakley/archive/2003/11/12/49645.aspx 我认为这不是最聪明的解决方案,但它能够工作。 - mati
2
作为包含HTML实体的字符串与escapeURI编码字符串不同,因此这些函数无法使用。 - Marcel Korpel
2
@Matias 注意,自2003年编写该函数以来,HTML(例如通过HTML 5规范)已添加了新的命名实体-例如,它无法识别“𝕫”。这是一个不断发展的规范问题;因此,您应选择一个实际正在维护的工具来解决它。 - Mark Amery
可能是如何使用jQuery解码HTML实体?的重复问题。 - lucascaro
我刚刚意识到很容易将这个问题与编码HTML实体混淆。我刚刚意识到我在这个问题上不小心发布了一个错误的答案!不过我已经删除了它。 - shreyasm-dev
34个回答

681

这里给出的大多数答案有一个巨大的缺点:如果您要转换的字符串不可信,则最终可能会导致跨站点脚本攻击(XSS)漏洞。对于被接受的答案中的函数,请考虑以下内容:

htmlDecode("<img src='dummy' onerror='alert(/xss/)'>");

这里的字符串包含一个未转义的HTML标签,因此htmlDecode函数将实际运行字符串内指定的JavaScript代码而不是对其进行解码。

通过使用DOMParser可以避免这种情况,所有现代浏览器都支持它:

function htmlDecode(input) {
  var doc = new DOMParser().parseFromString(input, "text/html");
  return doc.documentElement.textContent;
}

console.log(  htmlDecode("&lt;img src='myimage.jpg'&gt;")  )    
// "<img src='myimage.jpg'>"

console.log(  htmlDecode("<img src='dummy' onerror='alert(/xss/)'>")  )  
// ""

这个函数保证不会产生任何JavaScript代码的副作用。任何HTML标签都将被忽略,只返回文本内容。

兼容性注意:使用DOMParser解析HTML需要至少Chrome 30、Firefox 12、Opera 17、Internet Explorer 10、Safari 7.1或Microsoft Edge。因此,所有不支持此功能的浏览器都已经过期,截至2017年,唯一仍然偶尔可以看到的是旧版Internet Explorer和Safari版本(通常它们数量不足以引起注意)。


38
我认为这个回答是最好的,因为它提到了 XSS 漏洞。 - Константин Ван
2
请注意,根据您的参考资料,在 Firefox 12.0 之前 DOMParser 不支持 "text/html",而且仍有一些最新版本的浏览器甚至不支持 DOMParser.prototype.parseFromString()。根据您的参考资料,DOMParser 仍然是一项实验性技术,替代方法使用 innerHTML 属性,正如您在回答 我的方法 时指出的那样,它具有 XSS 漏洞(应由浏览器供应商修复)。 - PointedEars
5
@PointedEars: 在2016年谁还关心Firefox 12?问题在于Internet Explorer9.0及以下版本和Safari7.0及以下版本。如果能够承受不支持它们(希望现在是每个人都这样),那么DOMParser是最好的选择。如果不能承受,那么处理实体字符就是一个选项。 - Wladimir Palant
4
@PointedEars说:<script>标签没有被执行并不是一种安全机制,这个规则仅仅是避免了设置innerHTML可能会导致同步脚本运行的棘手的时间问题。清理HTML代码是一个棘手的事情,而innerHTML甚至不尝试这样做——因为网页可能实际上想要设置内联事件处理程序。这根本不是为不安全的数据而设计的机制,就此结束。 - Wladimir Palant
2
@ИльяЗеленько:你打算在一个紧密的循环中使用这段代码吗?或者为什么性能很重要?你的答案再次容易受到XSS攻击,真的值得吗? - Wladimir Palant
显示剩余16条评论

316

你需要解码所有编码的HTML实体吗,还是只需要处理&amp;本身?

如果只需要处理&amp;,则可以这样做:

var decoded = encoded.replace(/&amp;/g, '&');

如果您需要解码所有HTML实体,则可以在不使用jQuery的情况下完成:

var elem = document.createElement('textarea');
elem.innerHTML = encoded;
var decoded = elem.value;

请注意Mark在下面发表的评论,强调了这个答案早期版本中的安全漏洞,并建议使用textarea而不是div来减轻潜在的XSS漏洞。无论您使用jQuery还是纯JavaScript,这些漏洞都存在。


20
注意!这可能存在安全隐患。如果encoded='<img src="bla" onerror="alert(1)">',那么上面的代码片段将会显示一个警报。这意味着,如果您的编码文本来自用户输入,使用此代码片段进行解码可能会导致XSS漏洞。 - Mark Amery
@MarkAmery,我不是安全专家,但看起来如果在获取文本后立即将div设置为“null”,则img中的警报不会被触发- http://jsfiddle.net/Mottie/gaBeb/128/ - Mottie
4
我不确定您使用的是哪种浏览器,但在我的 OS X Chrome 浏览器中,alert(1) 仍会弹出。如果您想要一种更安全的方法,请尝试使用 textarea(参见:https://dev59.com/o3I-5IYBdhLWcg3wVWpi#31350391)。 - Mark Amery
2
如何在Node服务器上实现这个? - Mohammad Kermani
请注意,在此处使用<textarea>并不能防止所有浏览器中的XSS漏洞。输入</textarea><img src="bla" onerror="alert(1)">仍然存在问题。 - Wladimir Palant
显示剩余3条评论

200

编辑: 你应该使用DOMParser API,就像Wladimir建议的那样,我修改了我的先前答案,因为发布的函数引入了安全漏洞。

以下片段是旧答案的代码,进行了小修改:使用textarea代替div可以减少XSS漏洞,但在IE9和Firefox中仍然存在问题。

function htmlDecode(input){
  var e = document.createElement('textarea');
  e.innerHTML = input;
  // handle case of empty input
  return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
}

htmlDecode("&lt;img src='myimage.jpg'&gt;"); 
// returns "<img src='myimage.jpg'>"

基本上,我通过编程创建一个DOM元素,将编码的HTML分配给它的innerHTML,并从在innerHTML插入时创建的文本节点中检索nodeValue。由于它只创建一个元素但从未添加它,因此不会修改任何站点HTML。
它将跨浏览器工作(包括旧版浏览器)并接受所有HTML字符实体
编辑:这段代码的旧版本在IE中无法使用空白输入,如jsFiddle中所示(在IE中查看)。上面的版本适用于所有输入。
更新:似乎这对于大字符串不起作用,而且它还引入了一个安全漏洞,请参见评论。

1
@S.Mark:这是因为&apos;不属于HTML 4实体!请参考http://www.w3.org/TR/html4/sgml/entities.html和http://fishbowl.pastiche.org/2003/07/01/the_curse_of_apos/。 - Christian C. Salvadó
2
请参考@kender有关此方法安全性差的注释。 - Joseph Turian
2
请看我给 @kender 的注释,关于他所做的糟糕测试 ;) - Roatin Marth
31
这个函数存在安全隐患,即使元素未添加到DOM中,JavaScript代码也会运行。因此,只有在输入字符串可信时才应使用此函数。我添加了我的答案来解释这个问题并提供一个安全的解决方案。作为副作用,如果存在多个文本节点,则结果不会被截断。 - Wladimir Palant
1
如果JS未在浏览器中运行(即在Node中),则此方法无法工作。 - Mattia Rasulo
显示剩余12条评论

111

解析JavaScript中的HTML文本和其他内容的一种更现代化的选项是使用DOMParser API中的HTML支持(请参见MDN上的此处)。这使您能够使用浏览器的原生HTML解析器将字符串转换为HTML文档。自2014年末以来,该功能已得到所有主要浏览器新版本的支持。

如果我们只想解码某些文本内容,我们可以将其作为唯一内容放入文档正文中,解析文档,然后提取其 .body.textContent

var encodedStr = 'hello &amp; world';

var parser = new DOMParser;
var dom = parser.parseFromString(
    '<!doctype html><body>' + encodedStr,
    'text/html');
var decodedString = dom.body.textContent;

console.log(decodedString);

根据 DOMParser 草案规范,我们可以看到 JavaScript 未启用于解析文档中,因此我们可以在不涉及安全问题的情况下执行这种文本转换。

parseFromString(str, type) 方法必须按照以下步骤运行:type 可能有:

  • "text/html"

    使用 HTML 解析器 解析 str,并返回新创建的 Document

    脚本标记必须被设置为 "disabled"。

    注意

    script 元素将被标记为不可执行,而 noscript 的内容将被解析为标记。

虽然超出了此问题的范围,请注意,如果您正在移动已解析的 DOM 节点本身(而不仅仅是它们的文本内容)到实时文档 DOM,那么它们的脚本可能会重新启用,并且可能存在安全隐患。 我没有调查过,请小心谨慎。


8
有没有 NodeJs 的替代品? - questionasker
@coderInrRain: he, entitieshtml-entities - Dan Dascalescu

55

Matthias Bynens开发了一个相关库:https://github.com/mathiasbynens/he

示例:

console.log(
    he.decode("J&#246;rg &amp J&#xFC;rgen rocked to &amp; fro ")
);
// Logs "Jörg & Jürgen rocked to & fro"

我建议优先考虑使用它而不是涉及设置元素的HTML内容然后读取其文本内容的方法。这些方法可能有效,但是如果在不受信任的用户输入上使用,则会具有欺骗性和XSS机会。

如果你真的不能忍受加载库,你可以使用在此答案中描述的textarea hack来解决。与已经提出的各种类似方法不同,我所知道的这种方法没有安全漏洞:

function decodeEntities(encodedString) {
    var textArea = document.createElement('textarea');
    textArea.innerHTML = encodedString;
    return textArea.value;
}

console.log(decodeEntities('1 &amp; 2')); // '1 & 2'

但请注意我在链接答案中列出的影响类似方法的安全问题!这种方法是一种黑客技巧,未来对


1
Matthias Bynens的库he非常棒!非常感谢您的推荐! - Pedro A

40
如果您正在使用jQuery:
function htmlDecode(value){ 
  return $('<div/>').html(value).text(); 
}

否则,请使用Strictly Software的编码器对象,它具有出色的htmlDecode()函数。

68
请勿(重申不要)将此用于用户生成的除了用户生成的内容以外。如果值中有<script>标签,则脚本内容将被执行! - Michael Lorton
我无法在网站上找到该许可证。你知道这个许可证是什么吗? - TRiG
源代码头部有一个许可证,它是GPL。 - Chris Fulstow
6
是的,该函数为 XSS 开了后门:尝试使用 htmlDecode("<script>alert(12)</script> 123 >")。 - Dinis Cruz
$('<div/>') 的意思是什么? - Echo Yang
@Malvolio 并且为了给出一些原因/更深入的解释:如果用于不受信任的输入,这将打开一个 XSS漏洞 - rugk

29
你可以使用 Lodash 的 unescape / escape 函数。查看文档:https://lodash.com/docs/4.17.5#unescape
import unescape from 'lodash/unescape';

const str = unescape('fred, barney, &amp; pebbles');

str将变为'fred,barney和pebbles'


2
最好使用“import _unescape from 'lodash/unescape';”来避免与同名的已弃用的JavaScript函数:unescape发生冲突。 - Rick Penabella
最佳答案。我们在项目中已经使用了Lodash,它比他更正确地进行转义。 - Eugene Barsky
1
Lodash只会解码五个实体字符('&': '&', '<': '<', '>': '>', '"': '"', ''': "'"),并警告其用户:"注意:不会解码其他HTML实体字符。如果需要解码其他HTML实体字符,请使用第三方库he。" fwiw, 2¢, 等等。 - ruffin

23
var htmlEnDeCode = (function() {
    var charToEntityRegex,
        entityToCharRegex,
        charToEntity,
        entityToChar;

    function resetCharacterEntities() {
        charToEntity = {};
        entityToChar = {};
        // add the default set
        addCharacterEntities({
            '&amp;'     :   '&',
            '&gt;'      :   '>',
            '&lt;'      :   '<',
            '&quot;'    :   '"',
            '&#39;'     :   "'"
        });
    }

    function addCharacterEntities(newEntities) {
        var charKeys = [],
            entityKeys = [],
            key, echar;
        for (key in newEntities) {
            echar = newEntities[key];
            entityToChar[key] = echar;
            charToEntity[echar] = key;
            charKeys.push(echar);
            entityKeys.push(key);
        }
        charToEntityRegex = new RegExp('(' + charKeys.join('|') + ')', 'g');
        entityToCharRegex = new RegExp('(' + entityKeys.join('|') + '|&#[0-9]{1,5};' + ')', 'g');
    }

    function htmlEncode(value){
        var htmlEncodeReplaceFn = function(match, capture) {
            return charToEntity[capture];
        };

        return (!value) ? value : String(value).replace(charToEntityRegex, htmlEncodeReplaceFn);
    }

    function htmlDecode(value) {
        var htmlDecodeReplaceFn = function(match, capture) {
            return (capture in entityToChar) ? entityToChar[capture] : String.fromCharCode(parseInt(capture.substr(2), 10));
        };

        return (!value) ? value : String(value).replace(entityToCharRegex, htmlDecodeReplaceFn);
    }

    resetCharacterEntities();

    return {
        htmlEncode: htmlEncode,
        htmlDecode: htmlDecode
    };
})();

这是来自 ExtJS 源代码。


4
这段代码无法处理大多数的命名实体。例如,htmlEnDecode.htmlDecode('&euro;')应该返回'€',但它却返回了'&euro;' - Mark Amery

19

关键是利用浏览器的能力解码特殊的HTML字符,但不允许浏览器执行结果,就像实际的html一样...该函数使用正则表达式逐个识别和替换编码的HTML字符。

function unescapeHtml(html) {
    var el = document.createElement('div');
    return html.replace(/\&[#0-9a-z]+;/gi, function (enc) {
        el.innerHTML = enc;
        return el.innerText
    });
}

2
正则表达式可以使用/\&#?[0-9a-z]+;/gi更紧密地匹配,因为#只有在第二个字符出现时才会出现。 - TheAtomicOption
2
这是最佳答案。避免了XSS漏洞,也不会剥离HTML标签。 - Emmanuel

17

element.innerText也可以起到同样的作用。


是的 +1 targetElement = document.querySelector('.target'); targetElement.innerHTML = targetElement.textContent; - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接