deleting JSON array element in PHP, and re-encoding as JSON

前端 未结 3 1333
梦如初夏
梦如初夏 2021-01-15 13:10
function deleteNews($selected) {
    $file = file_get_contents(\'news.json\', true);

    $data=json_decode($file,true);
    unset($file);

foreach($selected as $ind         


        
3条回答
  •  离开以前
    2021-01-15 13:12

    TL;DR PHP arrays with all numerical keys, starting at zero, sorted and without holes are the only kind that will get rendered into a JSON array [ "square", "brackets" ]. All other kinds will become JSON dictionaries { "curly": "brackets" }.


    The reason for the behaviour you observe, and the reason why array_values appears to fix the problem (and in this case, actually does), is in the difference between PHP and JSON arrays and dictionaries.

    This is a PHP array with contiguous numerical keys:

    $a = array( "Apple", "Banana", "Canteloupe" );
    

    This is really

    $a = array( 0 => "Apple", 1 => "Banana", 2 => "Canteloupe" );
    

    In JSON this becomes an array:

    [ "Apple", "Banana", "Canteloupe" ]
    

    This instead is a PHP array with text keys (hash):

    $a = array( "a" => "Apple", "b" => "Banana", "c" => "Canteloupe" );
    

    In JSON this is a dictionary (note the curly braces):

    { "a": "Apple", "b": "Banana", "c": "Canteloupe" }
    

    Some operations change a PHP-array-rendered-as-array (ARA) to a -rendered-as-dictionary (ARD).

    And these operations are:

    • Adding a TEXT key to a NUMERIC array.

    • Having numeric keys NOT in uninterrupted sorted sequence 0...N.

    So these seem to be arrays, but are actually dictionaries:

    $a = array ( 2 => "Canteloupe", 1 => "Banana", 0 => "Apple" );
    
    $a = array ( 0 => "Apple", 2 => "Canteloupe" );
    
    $a = array ( 1 => "Banana", 0 => "Apple" );
    

    And this finally is the reason why deleting a key that is not the last one will thrash your array and make it a dictionary:

    0 1 2 3 => remove 3 => 0 1 2 => still an array!
    0 1 2 3 => remove 2 => 0 1 3 => NOT an array!
    0 1 2 3 => remove 2 => 0 1 3 => not an array => remove 3 => 0 1 => again an array!
    

    If you were to subject the array to any renumbering operation, or to array_values, you would get a purely numerical array again:

    /**
    * delete news items given their index.
    *
    * @param array $selected     the list of indexes (e.g. [ 0, 1, 7 ])
    * @return nothing
    */
    
    function deleteNews(array $selected = [ ]) {
        try {
            $news = array_values( // Reorder...
                array_diff_key( // ...all keys...
                    json_decode(file_get_contents('news.json', true), true), // ...in here...
                    array_flip($selected) // ...that are not in $selected.
                )
            );
        } catch (\Exception $e) {
            // TODO handle errors (e.g. file not found and bad JSON)
        }
        try {
            file_put_contents('news.json', json_encode($news));
        } catch (\Exception $e) {
            // TODO handle errors
        }
        // $url="./deleteNews.php";
        // redirect($url);
    }
    

    So if your code had removed the last indexes of your array, things would have appeared to be working. As soon as the unsetting created a hole in the array, or a text key was added...

    Same goes for sorting: [ 1 => "B", 0 => "A" ] is a JSON dictionary. Sort it preserving key associations to [ 0 => "A", 1 => "B" ], and it becomes a JSON array.

    One thing to note about your code: you read "news.json" with include-path set to true. But then you save it into the current directory. If you have "news.json" anywhere else in your path except in the current directory, you will end up having two news.json files; which of the two is then used depends on the include path itself.

    Local files and security

    Saving local files, as it happens here with news.json, is okay, but some precautions are often in order.

    In general it's best to place such "variable" files in a directory of their own (e.g. "./cache" or "./temp") with a suitable .htaccess to prevent direct reading/execution unless it's needed, and so that permissions may be enforced more clearly.

    For example one could use a "./data" directory, and have the PHP files everywhere else set to read-only for the web server; and finally instruct, say, the Apache Web server so that this one directory, while writeable by the web server, could not be easily used for exploiting the system:

    
        # You CANNOT ask for /data and have a directory listing. Just in case.
        options      -Indexes
        # You CANNOT save "news.php" and have it executed :-)
        php_flag     engine     off
    
    

    This way, even if someone succeeeded in uploading a file on your system and that file contained executable, malicious PHP code, that code would not be allowed to execute (not directly, at least).

    More on security: real-life experience

    Allowing direct access to files under the control of a third party, even indirectly, is always potentially dangerous - to others if not to you. For example, storing a JSON object with information gathered on a third site. I've just completed a successful attack test on a client's website. Mutatis mutandis,

    • I set up a page on my own web site with crafted data.
    • I logged in on WWW.CLIENT with my account (OK, so I left traces...)
    • I directed WWW.CLIENT to access EVIL.COM and get the data
    • From my own account I could see what was then rendered on WWW.CLIENT depending on escape codes, malformed UTF8 characters and other tricks
    • In the end (1) I was able to inject the command to load a CDN-like Javascript file (residing on EVIL.COM) and have it execute (2) in the security context of WWW.CLIENT
    • After several more machinations, I was able to direct a customer of WWW.CLIENT (still me with a test account, of course) to a crafted link on WWW.CLIENT that would silently supply EVIL.COM with the customer's authentication tokens (3), (4) allowing (e.g.) to modify extant orders by removing his merchandise, adding my own, and changing the delivery address (5).

    This exploited a total of five security flaws (for example 4: the authentication token was not linked to the owner's IP address, or was not regenerated at each transaction, and 5: changes to invoicing or delivery did not trigger a request to re-enter the credit card information). While you might think that such security flaws were, well, security flaws, and the client was unaware of them, they actually were known features, there by design, in the name of "usability" and "user convenience". Flaw 2 stems from an evil-counseled use of eval() in an ancillary library which isn't like to change anytime soon. In the end, changing local data storage capability (flaw 1) is likely to be the only intervention I'll be able to realistically sell to this particular customer.

提交回复
热议问题