在纯Java中转义HTML符号的推荐方式是什么?

回答 12 浏览 41.9万 2009-08-12

在纯Java代码中输出HTML时,有没有一种推荐的方法来转义<>"&字符?(除了手动做以下工作外,还有其他方法。)

String source = "The less than sign (<) and ampersand (&) must be escaped before using them in HTML";
String escaped = source.replace("<", "&lt;").replace("&", "&amp;"); // ...
Ben Lings 提问于2009-08-12
要注意的是,如果你输出到一个无引号的HTML属性中,其他字符如空格、制表符、退格符等......可以让攻击者引入没有列出任何字符的javascript属性。更多信息请参见OWASP XSS预防骗局表。Jeff Williams 2014-03-19
另外,在这段代码中,你应该在"&"之前转义",这样才能正常工作("&lt;"会被替换成"&amp;lt;",否则会呈现为"&lt;",而非"<")。source.replace("&", "&amp;").replace("<", "&lt;");Tey' 2020-02-23
12 个回答
#1楼
得票数 282

StringEscapeUtils,来自Apache Commons Lang

import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
// ...
String source = "The less than sign (<) and ampersand (&) must be escaped before using them in HTML";
String escaped = escapeHtml(source);

对于版本3来说。

import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4;
// ...
String escaped = escapeHtml4(source);
dfa 提问于2009-08-12
Luke S. 修改于2015-08-04
虽然StringEscapeUtils很好,但如果你想避免HTML/XML的空白规范化,它就不能为属性正确转义。请看我的回答,以了解更多细节。Adam Gent 2013-08-07
上面的例子已经坏了。现在使用 escapeHtml4() 方法。stackoverflowuser2010 2014-06-24
Guava 迷们请看下面的okranz'的回答George Hawkins 2015-01-27
如果网页采用UTF-8编码,那么我们只需要Guava的htmlEscaper,它只能转义以下五个ASCII字符:'"&<>。Apache的escapeHtml()也会替换包括重音在内的非ASCII字符,这在UTF-8网页中似乎是不必要的。zdenekca 2015-04-20
它现在已经在commons-lang3中被废弃。它被移至commons.apache.org/proper/commons-textDanny 2017-08-16
#2楼
得票数 158

一个替代Apache Commons的方法。使用SpringHtmlUtils.htmlEscape(String input)方法。

Adamski 提问于2009-08-12
skaffman 修改于2009-08-12
谢谢。我使用了它(而不是apache-commons2.6中的StringEscapeUtils.escapeHtml()),因为它让俄罗斯字符保持原样。Slava Semushin 2012-07-30
很高兴知道这一点。这几天我对Apache的东西很感兴趣。Adamski 2012-07-31
我也用过它,它也是按原样留下汉字的。vr3C 2015-06-09
而且它还对撇号进行编码,所以它实际上是有用的,不像apache的StringEscapeUtils那样。David Balažic 2018-09-20
#3楼
得票数 62

很好的简短方法。

public static String escapeHTML(String s) {
    StringBuilder out = new StringBuilder(Math.max(16, s.length()));
    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);
        if (c > 127 || c == '"' || c == '\'' || c == '<' || c == '>' || c == '&') {
            out.append("&#");
            out.append((int) c);
            out.append(';');
        } else {
            out.append(c);
        }
    }
    return out.toString();
}

根据https://stackoverflow.com/a/8838023/1199155(那里缺少放大器)。根据http://www.w3.org/TR/html4/sgml/entities.html,if子句中检查的四个字符是唯一低于128的字符。

Bruno Eberhard 提问于2014-08-10
Aloso 修改于2020-04-17
很好。它没有使用编码的"html版本"(例如:"á"将是"&aacute;"而不是"&#225;"),但由于数字的编码即使在IE7中也能工作,我想我不用担心。谢谢。nonzaprej 2017-09-04
为什么OP要求转义4个相关字符,你却要对所有这些字符进行编码?你在浪费CPU和内存。David Balažic 2018-09-20
你忘记了撇号。因此,人们可以在任何地方注入无引号的属性,而这段代码是用来转义属性值的。David Balažic 2018-09-20
当字符串中包含代理对时,例如emojis,这就不起作用了。Clashsoft 2020-08-14
#4楼
得票数 47

有一个更新的Apache Commons Lang库版本,它使用不同的包名(org.apache.commons.lang3)。现在StringEscapeUtils有不同的静态方法来转义不同类型的文档(http://commons.apache.org/proper/commons-lang/javadocs/api-3.0/index.html)。所以要转义HTML4.0版本的字符串。

import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4;

String output = escapeHtml4("The less than sign (<) and ampersand (&) must be escaped before using them in HTML");
Martin Dimitrov 提问于2011-07-19
Dawood ibn Kareem 修改于2013-09-23
不幸的是,没有任何东西适用于HTML 5,Apache文档也没有说明对HTML 5使用escapeHtml4是否合适。Paul Vincent Craven 2015-07-23
#5楼
得票数 46

对于那些使用Google Guava的人来说。

import com.google.common.html.HtmlEscapers;
[...]
String source = "The less than sign (<) and ampersand (&) must be escaped before using them in HTML";
String escaped = HtmlEscapers.htmlEscaper().escape(source);
okrasz 提问于2014-10-26
#6楼
得票数 41

对这一点要小心。在一个HTML文档中,有许多不同的 "语境"。元素内部、带引号的属性值、未带引号的属性值、URL属性、javascript、CSS等等。你需要对每一种情况使用不同的编码方法,以防止跨站脚本攻击(XSS)。请查看OWASP XSS预防骗局表,了解这些情况的细节。你可以在OWASP ESAPI库中找到这些语境的转义方法--https://github.com/ESAPI/esapi-java-legacy

Jeff Williams 提问于2013-02-15
Miha_x64 修改于2020-04-14
谢谢你指出,你希望对输出进行编码的context非常重要。术语"encode"也是一个比"escape"更合适的动词,而且。Escape意味着某种特殊的黑客行为,而"我如何encode这个字符串:XHTML属性/SQL查询参数/PostScript打印字符串/CSV输出域?Roboprog 2013-04-30
'Encode'和'escape'都被广泛用于描述这一点。术语"escape"一般用于在一个语法相关的字符之前添加一个"转义字符",例如用反斜杠转义一个引号字符";术语"encode"通常用于将一个字符翻译成不同形式,例如URL编码引号字符%22或HTML实体编码为&#x22或@quot。Jeff Williams 2014-03-19
owasp-esapi-java.googlecode.com/svn/trunk_doc/latest/index.html。链接现在已经中断了andrew pate 2017-01-05
为了节省你的搜索时间,寻找编码器类static.javadoc.io/org.owasp.esapi/esapi/2.0.1/org/owasp/esapi/…Jakub Bochenski 2019-08-12
#7楼
得票数 40

在安卓系统(API 16或更高版本)上,你可以。

Html.escapeHtml(textToScape);

或为较低的API而设。

TextUtils.htmlEncode(textToScape);
OriolJ 提问于2013-04-05
另请参见我的我的问题,了解这两者之间的区别。(@Muz )Jonas Czech 2016-02-16
#8楼
得票数 20

为了某些目的,HtmlUtils

import org.springframework.web.util.HtmlUtils;
[...]
HtmlUtils.htmlEscapeDecimal("&"); //gives &#38;
HtmlUtils.htmlEscape("&"); //gives &amp;
AUU 提问于2010-05-19
Eric 修改于2019-07-05
来自spring HtmlUtils的评论。* <p>对于一套全面的字符串转义工具,*可以考虑Apache Commons Lang和它的StringEscapeUtils类。* 我们在此不使用该类,以避免运行时对Commons Lang的依赖,*只用于HTML转义。此外,Spring的*HTML转义功能更加灵活,而且100%符合HTML 4.0。如果你已经在你的项目中使用了Apache commons,你应该使用apache的StringEscapeUtils。andreyro 2019-09-13
#9楼
得票数 14

org.apache.commons.lang3.StringEscapeUtils现在已被弃用。你现在必须使用org.apache.commons.text.StringEscapeUtils,通过

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-text</artifactId>
        <version>${commons.text.version}</version>
    </dependency>
Luca Stancapiano 提问于2018-05-30
#10楼
得票数 13

虽然@dfa对org.apache.commons.lang.StringEscapeUtils.escapeHtml的回答很好,而且我过去也用过它,但它不应该用于转义HTML(或XML)属性,否则空白将被规范化(意味着所有相邻的空白字符都变成了一个空格)。

我知道这一点,因为我的库(JATL)曾因属性没有保留空格而被提出过错误。因此,我有一个滴入(复制粘贴)的类(我从JDOM中偷了一些),区分了属性和元素内容的转义

虽然这在过去可能不那么重要(正确的属性转义),但鉴于HTML5的data-属性的使用,它越来越成为人们关注的焦点。

Adam Gent 提问于2013-08-07
#11楼
得票数 1

Java 8以上的解决方案。

public static String escapeHTML(String str) {
    return str.chars().mapToObj(c -> c > 127 || "\"'<>&".indexOf(c) != -1 ?
       "&#" + c + ";" : String.valueOf((char) c)).collect(Collectors.joining());
}

String#chars返回String中的char值的IntStream。然后我们可以使用mapToObj来转义字符代码大于127的字符(非ASCII字符)以及双引号(")、单引号(')、左角括号(<)、右角括号(>)和安培号(&)。Collectors.joiningString串联起来。

为了更好地处理Unicode字符,可以使用String#codePoints来代替。

public static String escapeHTML(String str) {
    return str.codePoints().mapToObj(c -> c > 127 || "\"'<>&".indexOf(c) != -1 ?
            "&#" + c + ";" : new String(Character.toChars(c)))
       .collect(Collectors.joining());
}
Unmitigated 提问于2021-03-02
Unmitigated 修改于2021-03-02
#12楼
得票数 1

大多数库都提供了转义功能,包括数百个符号和数千个非ASCII字符,这不是你在UTF-8世界中想要的东西。

另外,正如Jeff Williams所指出的,没有单一的 "转义HTML "选项,而是有几个上下文。

假设你从不使用非引号属性,并牢记存在不同的上下文,它已经写了我自己的版本。

private static final long TEXT_ESCAPE =
        1L << '&' | 1L << '<';
private static final long DOUBLE_QUOTED_ATTR_ESCAPE =
        TEXT_ESCAPE | 1L << '"';
private static final long SINGLE_QUOTED_ATTR_ESCAPE =
        TEXT_ESCAPE | 1L << '\'';
private static final long ESCAPES =
        DOUBLE_QUOTED_ATTR_ESCAPE | SINGLE_QUOTED_ATTR_ESCAPE;

// 'quot' and 'apos' are 1 char longer than '#34' and '#39'
// which I've decided to use
private static final String REPLACEMENTS = "&#34;&amp;&#39;&lt;";
private static final int REPL_SLICES = /*  [0,   5,   10,  15, 19) */
        5<<5 | 10<<10 | 15<<15 | 19<<20;
// These 5-bit numbers packed into a single int
// are indices within REPLACEMENTS which is a 'flat' String[]

private static void appendEscaped(
        Appendable builder, CharSequence content, long escapes) {
    try {
        int startIdx = 0, len = content.length();
        for (int i = 0; i < len; i++) {
            char c = content.charAt(i);
            long one;
            if (((c & 63) == c) && ((one = 1L << c) & escapes) != 0) {
            // -^^^^^^^^^^^^^^^   -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            // |                  | take only dangerous characters
            // | java shifts longs by 6 least significant bits,
            // | e. g. << 0b110111111 is same as >> 0b111111.
            // | Filter out bigger characters

                int index = Long.bitCount(ESCAPES & (one - 1));
                builder.append(content, startIdx, i /* exclusive */).append(
                        REPLACEMENTS,
                        REPL_SLICES >>> (5 * index) & 31,
                        REPL_SLICES >>> (5 * (index + 1)) & 31
                );
                startIdx = i + 1;
            }
        }
        builder.append(content, startIdx, len);
    } catch (IOException e) {
        // typically, our Appendable is StringBuilder which does not throw;
        // also, there's no way to declare 'if A#append() throws E,
        // then appendEscaped() throws E, too'
        throw new UncheckedIOException(e);
    }
}

考虑从Gist中复制粘贴,不受行长限制

UPD。正如另一个答案所建议的,>转义是没有必要的;另外,"attr='…'内也是允许的。我已经相应地更新了代码。

你可以自己检查一下

<!DOCTYPE html>
<html lang="en">
<head><title>Test</title></head>
<body>

<p title="&lt;&#34;I'm double-quoted!&#34;>">&lt;"Hello!"></p>
<p title='&lt;"I&#39;m single-quoted!">'>&lt;"Goodbye!"></p>

</body>
</html>
Miha_x64 提问于2020-04-14
Miha_x64 修改于2021-10-27
标签