Jump to content


Photo

json_encode() is *not* a security feature (or: How to pass PHP values to JavaScript)


  • Please log in to reply
1 reply to this topic

#1 Jacques1

Jacques1
  • Gurus
  • Turtles all the way down
  • 3,702 posts

Posted 22 January 2015 - 02:25 AM

Since there have been some debates about how to safely pass PHP values to JavaScript, I hope I can clarify a few things.

 

One suggestion that kept recurring was to simply run the value through json_encode() and then inject the result into a script element. The JSON-encoding is supposed to (magically?) prevent cross-site scripting vulnerabilities. And indeed it seemingly works, because naïve attacks like trying to inject a double quote will fail.

 

Unfortunately, this approach doesn't work at all and is fundamentally wrong for several reasons:

  1. json_encode() was never intended to be a security function. It simply builds a JSON object from a value. And the JSON specification doesn't make any security promises either. So even if the function happens to prevent some attack, this is implementation-specific and may change at any time.
  2. JSON doesn't know anything about HTML entities. The encoder leaves entities like " untouched, not realizing that this represents a double quote which is dangerous in a JavaScript context.
  3. The json_encode() function is not encoding-aware, which makes it extremely fragile and unsuitable for any security purposes. Some of you may know this problem from SQL-escaping: There used to be a function called mysql_escape_string() which was based on a fixed character encoding instead of the actual encoding of the database connection. This quickly turned out to be a very bad idea, because a mismatch could render the function useless (e. g. the infamous GBK vulnerability). So back in 2002(!), the function was abandoned in favor of mysql_real_escape_string(). Well, json_encode() is like the old mysql_escape_string() and suffers from the exact same issues.

Any of those issues can be fatal and enable attackers to perform cross-site scripting, as demonstrated below.

 

 

 

1)

 

The entire “security” of json_encode() is based on side-effects. For example, the current implementation happens to escape forward slashes. But the JSON standard doesn't mandate this in any way, so this feature could be removed at any time (it can also be disabled at runtime). If it does get disabled, then your application is suddenly wide open to even the most trivial cross-site scripting attacks:

<?php

header('Content-Type: text/html; charset=UTF-8');

$input = '</script><script>alert(String.fromCharCode(88, 83, 83));</script><script>';

?>
<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8">
        <title>XSS</title>
    </head>
    <body>
        <script>
            var x = <?= json_encode($input, JSON_UNESCAPED_SLASHES) ?>;
        </script>
    </body>
</html>

2)

 

In XHTML, a script element works like any other element, so HTML entities like &quot; are replaced with their actual characters (in this case a double quote). But JSON does not recognize HTML entities, so an attacker can use them to bypass json_encode() and inject arbitrary characters:

<?php

header('Content-Type: application/xhtml+xml; charset=UTF-8');

$input = "&quot;;alert('XSS');&quot;";

?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>XSS</title>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <script type="text/javascript">
            var x = <?= json_encode($input) ?>;
        </script>
    </body>
</html>

3)

 

json_encode() blindly assumes that the input and the output should always be UTF-8. If you happen to use a different encoding, or if an attacker manages to trigger a specific encoding, you're again left with no protection at all:

<?php

header('Content-Type: text/html; charset=UTF-7');

$input = '+ACIAOw-alert(+ACI-XSS+ACI)+ADsAIg-';

?>
<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-7">
        <title>XSS</title>
    </head>
    <body>
        <script>
            var x = <?= json_encode($input) ?>;
        </script>
    </body>
</html>

(This particular example only works in Internet Explorer.)

 

 

 

I hope this makes it very clear that json_encode() is not a security feature in any way. Relying on it is conceptually wrong and simply a very bad idea.

 

It's generally not recommended to inject code directly into a script element, because any mistake or bug will immediately lead to a cross-site scripting vulnerability. It's also very difficult to do it correctly, because there are special parsing rules and differences between the various flavors of HTML. If you try it, you're asking for trouble.

 

So how should one pass PHP values to JavaScript? By far the most secure and robust approach is to simply use Ajax: Since Ajax cleanly separates the data from the application logic, the value can't just “leak” into a script context. This is essentially like a prepared statement.

 

If you're into micro-optimization and cannot live with the fact that Ajax may need an extra request, there's an alternative approach by the OWASP: You can JSON-encode the data, HTML-escape the result, put the escaped content into a hidden div element and then parse it with JSON.parse():

<?php

header('Content-Type: text/html; charset=UTF-8');

$input = 'bar';

?>
<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8">
        <title>XSS</title>
        <style>
            .hidden {
                display: none;
            }
        </style>
    </head>
    <body>
        <div id="my-data" class="hidden">
            <?php
                $json_object = json_encode(array(
                    'foo' => $input,
                ));

                // HTML-escape the JSON object
                echo htmlspecialchars($json_object, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
            ?>
        </div>
        <script>
            var data = JSON.parse(document.getElementById('my-data').innerHTML);
            
            alert('The following value has been safely passed to JavaScript: ' + data.foo);
        </script>
    </body>
</html>


#2 Jacques1

Jacques1
  • Gurus
  • Turtles all the way down
  • 3,702 posts

Posted 22 January 2015 - 02:25 AM

Besides this concrete case, one point I'm really trying to get across is this: When dealing with security, don't just do what you think is the bare minimum. Chances are you've overlooked some tiny little detail, and then you may be screwed.

 

Don't assume that you know everything about HTML or SQL or whatever. Don't assume that things will always go as planned. Don't assume that a technique is secure until somebody has proven the opposite. Use robust security and established solutions (like those from the OWASP). Avoid risks instead of fighting with vulnerabilities.






0 user(s) are reading this topic

0 members, 0 guests, 0 anonymous users