En Java, nous avons du code qui prend un objet Java complexe et le sérialise en json. Il écrit ensuite ce json directement dans le balisage d’une page, dans une balise de script, en l’affectant à une 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);
L'objet complexe peut contenir un contenu d'utilisateur final, ce qui pourrait nous ouvrir aux attaques XSS.
Comment puis-je obtenir une version json de l'objet Java complexe contenant chaque attribut json HTML échappé, pour se protéger contre l'injection XSS?
J'ai lu le Guide OWASP XSS et le meilleur que j'ai trouvé jusqu'ici est le suivant: HTML échappe à la chaîne JSON entière, puis annule les guillemets afin qu'elle puisse être attribuée à une variable en javascript. Je suis sûr qu'il existe de meilleures façons de le faire, mais cela semble fonctionner. Aucune suggestion?
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";
}
}
Une approche possible pourrait consister à parcourir les entrées d'objet et à échapper individuellement chaque clé et valeur une fois le nœud construit par la bibliothèque de votre choix.
Suite à mon commentaire ci-dessus, j'ai implémenté une solution récursive simple utilisant à la fois Jackson (de votre question) et GSON , une bibliothèque différente où les objets sont légèrement plus faciles à construire et où le code est plus lisible. . Le mécanisme d'échappement utilisé est le 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;
}
}
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 { // 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;
}
}
Exemple d'entrée (les deux bibliothèques):
{
"nested": {
"<html>": "<script>(function(){alert('xss1')})();</script>"
},
"xss": "<script>(function(){alert('xss2')})();</script>"
}
Exemple de sortie (les deux bibliothèques):
{
"nested": {
"<html>": "<script>(function(){alert('xss1')})();</script>"
},
"xss": "<script>(function(){alert('xss2')})();</script>"
}
Je pense que la réponse de Paul Benn est la meilleure approche dans l’ensemble, mais si vous ne voulez pas parcourir les nœuds JSON, vous pouvez envisager d’utiliser Encode.forHtmlContent , qui n’échappe pas aux guillemets. Je pense que c'est probablement sans danger car je ne vois pas en quoi l'introduction d'une citation supplémentaire dans une chaîne citée pourrait provoquer un exploit. Je laisserai au lecteur le soin de revoir la documentation et de décider par lui-même!
<dependency org="org.owasp.encoder" name="encoder" rev="1.2.1"/>
et du code pour faire le codage HTML
private String objectToJson(Object value)
{
String result;
try
{
result = jsonWriter.writeValueAsString(value);
return Encode.forHtmlContent(result);
}
catch (JsonProcessingException e)
{
return "null";
}
}