Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java and XSS: How to html escape a JSON string to protect against XSS?

In Java, we've got some code that takes a complex java object and serializes it to json. It then writes that json directly to the markup of a page, in a script tag, assigning it to a variable.

// 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);

The complex object can have end user content in it, which could open us up to XSS attacks.

How can I get a json version of the complex java object that has each json attribute HTML escaped, to protect against XSS injection?

I've read the OWASP XSS Guide and the best I've come up with so far is this, which HTML escapes the entire JSON string, then undoes the quotes, so it can be assigned to a variable in javascript. I'm sure there are better ways to do this, but this seems to work. Any suggestions?

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";
    }
}
like image 519
Brad Parks Avatar asked Jun 14 '18 12:06

Brad Parks


People also ask

Does escaping HTML prevent XSS?

Escaping is the primary means to avoid cross-site scripting attacks. When escaping, you are effectively telling the web browser that the data you are sending should be treated as data and should not be interpreted in any other way.

Is JSON safe from XSS?

The functionality to translate a JavaScript object into a string-based representation is hardly thrilling. But when the stars align, a simple JSON serialization operation can result in a significant XSS vulnerability.

Is escaping enough for XSS?

Escaping won't fix XSS either But for HTML injection (XSS), it is necessary to sanitize—or more specifically, to escape—the user-controlled data. However, escaping is context-dependent. That is to say, if the same data is used in multiple places, a different escaping may be necessary.

What is a best practice for avoiding an XSS attack?

How to prevent XSS attacks. To prevent XSS attacks, your application must validate all the input data, make sure that only the allowlisted data is allowed, and ensure that all variable output in a page is encoded before it is returned to the user.


2 Answers

A possible approach could be to iterate over the object entries and individually escape each key and value once the node is constructed by your chosen library.

Following my comment above, I've implemented a simple recursive solution using both Jackson (from your question) and GSON, a different library where objects are slightly easier to construct and the code is more readable. The escaping mechanism used is the OWASP Java Encoder:

Jackson

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;
    }
}

Sample input (both libraries):

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

Sample output (both libraries):

{
    "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;"
}
like image 139
Paul Benn Avatar answered Oct 14 '22 16:10

Paul Benn


Updating Paul Benn's answer of the Gson version to include json value being an array

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;
    }
}

like image 45
JKRo Avatar answered Oct 14 '22 16:10

JKRo