Java 和 XSS:如何对 JSON 字符串进行 html 转义以防止 XSS?

问题描述 投票:0回答:5

在 Java 中,我们有一些代码可以获取复杂的 java 对象并将其序列化为 json。然后,它将 json 直接写入页面的标记中的脚本标记中,并将其分配给变量。

// Get object as JSON using Jackson
ObjectWriter jsonWriter = new ObjectMapper().writer().withDefaultPrettyPrinter();
String json = jsonWriter.writeValueAsString(complexObject);

// Write JSON out to page, and assign it to a javascript variable.
Writer out = environment.getOut();
out.write("var data = " + json);

复杂对象中可能包含最终用户内容,这可能会让我们面临 XSS 攻击。

如何获取每个 json 属性 HTML 转义的复杂 java 对象的 json 版本,以防止 XSS 注入?

我已经阅读了 OWASP XSS 指南,到目前为止我想到的最好的就是这个,其中 HTML 转义了整个 JSON 字符串,然后撤消引号,因此可以将其分配给 javascript 中的变量。我确信有更好的方法可以做到这一点,但这似乎有效。有什么建议吗?

private String objectToHtmlEscapedJson(Object value) {
    try {
        String result = jsonWriter.writeValueAsString(value);
        result = StringEscapeUtils.escapeHtml(result);
        result = result.replace(""", "\"");
        return result;
    } catch (JsonProcessingException e) {
        return "null";
    }
}
java json jackson xss
5个回答
12
投票

一种可能的方法是迭代对象条目,并在您选择的库构建节点后单独转义每个键和值。

根据上面的评论,我使用 Jackson (来自你的问题)和 GSON 实现了一个简单的递归解决方案,这是一个不同的库,其中对象更容易构造,并且代码更具可读性。使用的转义机制是 OWASP Java Encoder:

杰克逊

private static JsonNode clean(JsonNode node) {
    if(node.isValueNode()) { // Base case - we have a Number, Boolean or String
        if(JsonNodeType.STRING == node.getNodeType()) {
            // Escape all String values
            return JsonNodeFactory.instance.textNode(Encode.forHtml(node.asText()));
        } else {
            return node;
        }
    } else { // Recursive case - iterate over JSON object entries
        ObjectNode clean = JsonNodeFactory.instance.objectNode();
        for (Iterator<Map.Entry<String, JsonNode>> it = node.fields(); it.hasNext(); ) {
            Map.Entry<String, JsonNode> entry = it.next();
            // Encode the key right away and encode the value recursively
            clean.set(Encode.forHtml(entry.getKey()), clean(entry.getValue()));
        }
        return clean;
    }
}

GSON

private static JsonElement clean(JsonElement elem) {
    if (elem.isJsonPrimitive()) { // Base case - we have a Number, Boolean or String
        JsonPrimitive primitive = elem.getAsJsonPrimitive();
        if(primitive.isString()) {
            // Escape all String values
            return new JsonPrimitive(Encode.forHtml(primitive.getAsString()));
        } else {
            return primitive;
        }
    } else if (elem.isJsonArray()) { // We have an array - GSON requires handling this separately
        JsonArray cleanArray = new JsonArray();
        for(JsonElement arrayElement: elem.getAsJsonArray()) {
            cleanArray.add(clean(arrayElement));
        }
        return cleanArray;
    } else { // Recursive case - iterate over JSON object entries
        JsonObject obj = elem.getAsJsonObject();
        JsonObject clean = new JsonObject();
        for(Map.Entry<String, JsonElement> entry :  obj.entrySet()) {
            // Encode the key right away and encode the value recursively
            clean.add(Encode.forHtml(entry.getKey()), clean(entry.getValue()));
        }
        return clean;
    }
}

示例输入(两个库):

{
    "nested": {
        "<html>": "<script>(function(){alert('xss1')})();</script>"
    },
    "xss": "<script>(function(){alert('xss2')})();</script>"
}

示例输出(两个库):

{
    "nested": {
        "&lt;html&gt;": "&lt;script&gt;(function(){alert(&#39;xss1&#39;)})();&lt;/script&gt;"
    },
    "xss": "&lt;script&gt;(function(){alert(&#39;xss2&#39;)})();&lt;/script&gt;"
}

5
投票

我认为Paul Benn的答案是总体上最好的方法,但如果您不想迭代json节点,您可以考虑使用Encode.forHtmlContent,它不会转义引号。我觉得这可能是安全的,因为我无法想象在带引号的字符串中引入额外的引号会如何导致漏洞利用。我将留给读者查看文档并自行决定!

ivy.xml

<dependency org="org.owasp.encoder" name="encoder" rev="1.2.1"/>

以及一些进行 html 编码的代码

private String objectToJson(Object value)
{
    String result;
    try
    {
        result = jsonWriter.writeValueAsString(value);
        return Encode.forHtmlContent(result);
    }
    catch (JsonProcessingException e)
    {
        return "null";
    }
}

2
投票

更新 Paul Benn 的 Gson 版本的答案,以包含作为数组的 json 值

private static JsonElement clean(JsonElement elem) {
    if(elem.isJsonPrimitive()) { // Base case - we have a Number, Boolean or String
        JsonPrimitive primitive = elem.getAsJsonPrimitive();
        if(primitive.isString()) {
            // Escape all String values
            return new JsonPrimitive(Encode.forHtml(primitive.getAsString()));
        } else {
            return primitive;
        }
    }  else if( elem.isJsonArray()  ) { // If the object is an array  "cars": ["toyota", "nissan", "bmw"]
        JsonArray jsonA = elem.getAsJsonArray();
        JsonArray cleanedNewArray = new JsonArray();
        for(JsonElement jsonAE: jsonA) {
            cleanedNewArray.add(clean(jsonAE));
        }
        return cleanedNewArray;
    } else { // Recursive case - iterate over JSON object entries
        JsonObject obj = elem.getAsJsonObject();
        JsonObject clean = new JsonObject();
        for(Map.Entry<String, JsonElement> entry :  obj.entrySet()) {
            // Encode the key right away and encode the value recursively
            clean.add(Encode.forHtml(entry.getKey()), clean(entry.getValue()));
        }
        return clean;
    }
}


2
投票

使用 Jackson 和 Esapi 添加 JKRo 的版本。

private JsonNode clean(JsonNode node, ObjectMapper mapper) {
    if(node.isValueNode()) { // Base case - we have a Number, Boolean or String
        if(JsonNodeType.STRING == node.getNodeType()) {
            // Escape all String values
            return JsonNodeFactory.instance.textNode(ESAPI.encoder().encodeForHTML(node.asText()));
        } else {
            return node;
        }
    } else if(node.isArray()) { // If the object is an array  "cars": ["toyota", "nissan", "bmw"]
        ArrayNode cleanedNewArray = mapper.createArrayNode();
        for (final JsonNode objNode : node) {
            cleanedNewArray.add(clean(objNode, mapper));
        }
        return cleanedNewArray;
    } else { // Recursive case - iterate over JSON object entries
        ObjectNode clean = JsonNodeFactory.instance.objectNode();

        for (Iterator<Map.Entry<String, JsonNode>> it = node.fields(); it.hasNext(); ) {
            Map.Entry<String, JsonNode> entry = it.next();
            // Encode the key right away and encode the value recursively
            clean.set(ESAPI.encoder().encodeForHTML(entry.getKey()), clean(entry.getValue(), mapper));
        }
        return clean;
    }
}

请求正文:

{
"param1": "<input class='btn btn-default' value='0' placeholder='Ingrese sus datos'></input>",
"param3": [
{
    "nombre" : "<input class='btn btn-default' value='0' placeholder='Ingrese sus datos'></input>",
    "apellido": "<script>alert('Hola mundex');</script>"
},
{
    "param4": {
        "nombre" : "<input class='btn btn-default' value='0' placeholder='Ingrese sus datos'></input>",
        "apellido": "<script>alert('Hola mundex');</script>"
    }
}],
"param2": "alert('Hola')"

}

响应正文:

{
"param1": "&lt;input class&#x3d;&#x27;btn btn-default&#x27; value&#x3d;&#x27;0&#x27; placeholder&#x3d;&#x27;Ingrese sus datos&#x27;&gt;&lt;&#x2f;input&gt;",
"param3": [
    {
        "nombre": "&lt;input class&#x3d;&#x27;btn btn-default&#x27; value&#x3d;&#x27;0&#x27; placeholder&#x3d;&#x27;Ingrese sus datos&#x27;&gt;&lt;&#x2f;input&gt;",
        "apellido": "&lt;script&gt;alert&#x28;&#x27;Hola mundex&#x27;&#x29;&#x3b;&lt;&#x2f;script&gt;"
    },
    {
        "param4": {
            "nombre": "&lt;input class&#x3d;&#x27;btn btn-default&#x27; value&#x3d;&#x27;0&#x27; placeholder&#x3d;&#x27;Ingrese sus datos&#x27;&gt;&lt;&#x2f;input&gt;",
            "apellido": "&lt;script&gt;alert&#x28;&#x27;Hola mundex&#x27;&#x29;&#x3b;&lt;&#x2f;script&gt;"
        }
    }
],
"param2": "alert&#x28;&#x27;Hola&#x27;&#x29;"

}


0
投票

老问题,但我有类似的问题。首先,从你的问题来看,不清楚你打算如何处理变量“data”内的内容。如果它将用于使用危险的 DOM 方法创建 HTML 标记(如 OWASP 指南中所述),那么可以使用提供的答案。

但是也许您需要保留 json 数据的实际值以供进一步处理(包括其危险字符),那么当 json 包含结束 SCRIPT 标记(这会破坏浏览器的 javascript 解析并可能会导致错误)时,示例中唯一明显的问题就会发生。允许包含攻击者提供的新脚本。)杰克逊不会检测到它!

使用base64编码的解决方案:

String json = jsonWriter.writeValueAsString(complexObject);
String encoded = Base64.getEncoder().encodeToString(json.getBytes("UTF-8"));
out.write("var encodedData = '" + json + "';");
out.write("var data = JSON.parse(window.atob(encodedData));");

另请参阅: 如何让 Jackson 在输出字符串中转义 ?

© www.soinside.com 2019 - 2024. All rights reserved.