【问题标题】:Java and XSS: How to html escape a JSON string to protect against XSS?Java 和 XSS:如何 html 转义 JSON 字符串以防止 XSS?
【发布时间】:2018-06-14 12:31:31
【问题描述】:

在 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 攻击。

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

我已经阅读了OWASP XSS Guide,到目前为止我想出的最好的就是这个,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";
    }
}

【问题讨论】:

  • 另一种方法可能是遍历对象条目并单独转义每个键和值 - 这样也减少了将格式错误的数据传递给您的 JS 的机会。既然您阅读了 OWASP 指南,您可能还想看看 their anti-XSS encoder
  • 谢谢!这是一个很好的建议。我正在考虑它,但希望 owasp 或其他人已经完成了这项任务。

标签: java json jackson xss


【解决方案1】:

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

根据我上面的评论,我使用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;"
}

【讨论】:

    【解决方案2】:

    我认为Paul Benn's answer 是总体上最好的方法,但是如果您不想遍历 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";
        }
    }
    

    【讨论】:

      【解决方案3】:

      更新 Gson 版本的Paul Benn's 答案以包含作为数组的 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;
          }
      }
      
      

      【讨论】:

      • 您可能想要描述更新中的新内容。
      【解决方案4】:

      使用 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;"
      

      }

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-02-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-09-17
        • 1970-01-01
        • 2011-01-01
        相关资源
        最近更新 更多