web-dev-qa-db-ja.com

JavaおよびXSS:XSSから保護するためにJSON文字列をHTMLエスケープする方法は?

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攻撃にさらされる可能性があります。

XSSインジェクションから保護するために、複合体のjsonバージョンを取得するにはどうすればよいですかJava各json属性がHTMLでエスケープされているオブジェクト?

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";
    }
}
6
Brad Parks

考えられるアプローチは、オブジェクトエントリを反復処理し、選択したライブラリによってノードが構築されたら、各キーと値を個別にエスケープすることです。

上記のコメントに続いて、 Jackson (あなたの質問から)と [〜#〜] gson [〜#〜] の両方を使用して、単純な再帰的ソリューションを実装しました。オブジェクトの作成が少し簡単で、コードが読みやすいライブラリ。使用されるエスケープメカニズムは OWASP Javaエンコーダー

ジャクソン

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 { // 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;"
}
4
Paul Benn

更新 Paul Benn's 配列であるjson値を含めるための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()  ) { // 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;
    }
}

1
JKRo

Paul Bennの答え が全体的に最善のアプローチだと思いますが、jsonノードを繰り返し処理したくない場合は、 Encode.forHtmlContent の使用を検討できます。 t引用符をエスケープします。引用符で囲まれた文字列に追加の引用符を導入すると悪用が発生する可能性があるとは考えられないため、これはおそらく安全だと思います。ドキュメントを確認して自分で決めるのは読者に任せます!

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";
    }
}
1
Brad Parks