CarCzar
CarCzar

Reputation: 150

Http 406 error on javascript ajax post. Accept header set to accept all

I am getting an error from the server when I try to make an AJAX request that I cannot figure out. It is an HTTP 406 error. From what I understand this means that the server cannot send a response in the format that the Accept header indicates. But from what I can see, I am Accepting */* (wildcard).

You can see the details of my HTTP request in this image. https://i.sstatic.net/fTfBf.png My HTTP Request

This is my PHP code for the page:

<body>
<div id="container">

             (snip - menu and header)

    <div id="notify-saved">
        <div class="width">
            <span>Saved!</span>
        </div>
    </div>
    <div id="confirm-delete">
        <div id="confirm-delete-content" class="width">
        </div>
    </div>
    <div id="nav-separator">
    </div>
    <div id="body" class="width">

        <section id="content">
            <br />
    <form method="POST" id='template-form' action="post-actions.php" >
        <input type='hidden' name='id' value="15" />
        <input type='text' name='name' value="default-scripting" placeholder='Name' />
            <br />
            <br />
        <input type='checkbox' name='major' id="majorminor" />
        <textarea name='value' style="width:100%;">&lt;script src=&quot;http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js&quot;&gt;&lt;/script&gt;</textarea>
            <br />
            <br />

        <button name='action' value="Update Template" id='update-button' >Update</button>



    </form>
            </section>
        </div>
    </div>
</body>

Javascript:

// Attach a submit handler to the form
$( "#update-button" ).click( function( event ) {
    // Stop form from submitting normally
    event.preventDefault();

    // Get some values from elements on the page:
    var $form = $('#template-form');
    var template_id = $form.find( "input[name='id']" ).val();
    var template_name = $form.find( "input[name='name']" ).val();
    var template_value = $form.find( "textarea[name='value']" ).val();

    var template_is_major = 'minor';
    if( $( "input[name='major']" ).prop('checked') ){
        template_is_major = "major";
    }

    var url = $form.attr( "action" );

    // Send the data using post
    var posting = $.post( url, {  id: template_id, name: template_name, value: template_value, major: template_is_major, action: "Update Template"  } );

    // Show that the request has completed successfully
    posting.done(function( data ) {
        $('#notify-saved').slideDown(200).delay(500).slideUp(500);
    });
});

.

Also of relevance is the fact that this error only happens some of the time, for instance if the template-value submitted is

&lt;script src=&quot;http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js&quot;&gt;&lt;/script&gt;

then there will be an error, but if the leading < is omitted, then there is no error:

script src=&quot;http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js&quot;&gt;&lt;/script&gt;

The receiving php looks like this (post-actions.php):

<?php 

    // ugly-@$$ anti-magicquotes hackk
    if (get_magic_quotes_gpc()) {
        $process = array(&$_GET, &$_REQUEST, &$_COOKIE, &$_REQUEST);
        while (list($key, $val) = each($process)) {
            foreach ($val as $k => $v) {
                unset($process[$key][$k]);
                if (is_array($v)) {
                    $process[$key][stripslashes($k)] = $v;
                    $process[] = &$process[$key][stripslashes($k)];
                } else {
                    $process[$key][stripslashes($k)] = stripslashes($v);
                }
            }
        }
        unset($process);
    }


    require_once('database.php');
    $dbo = new Database();

    if( isset( $_REQUEST['action'] ) ){

        switch( $_REQUEST['action'] ){

            (snip - many irrelevant cases)

            case "Update Template":
                $dbo->UpdateTemplate( $_REQUEST[DB_TEMPLATES_ID], $_REQUEST[DB_TEMPLATES_NAME],
                $_REQUEST[DB_TEMPLATES_VALUE], $_REQUEST[DB_TEMPLATES_MAJOR] );
                break;



        }

    }

And the database class:

<?php 

require_once ("config.php");
require_once ("database-contract.php");

class Database {

    protected $db;
    protected $pdo;

    function __construct(){
        $this->db = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
        if($this->db->connect_errno > 0){
            die('Unable to connect to database [' . $this->db->connect_error . ']');
        }

        $this->pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8', DB_USER, DB_PASS );
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    }

    function __destruct(){
        $this->db->close();
        $this->pdo = null;
    }


        $query = 'INSERT INTO `'.DB_TEMPLATES.'` ( `'.DB_TEMPLATES_NAME.'` ) VALUES ( :name )';

        $stmt = $this->pdo->prepare($query);
        $stmt->bindValue(":name", $name, PDO::PARAM_INT);
        $stmt->execute();

    }

    function updateTemplate($id, $name, $value, $major ){

        if( $major == "major" ){ $major = 1; }
        else{ $major = 0; }

        $query = 'UPDATE `'.DB_TEMPLATES.'` 
                    SET 
                    `'.DB_TEMPLATES_NAME.'`=:name,
                    `'.DB_TEMPLATES_VALUE.'`=:value,
                    `'.DB_TEMPLATES_MAJOR.'`=:major
                    WHERE
                    `'.DB_TEMPLATES_ID.'`=:id';

        $stmt = $this->pdo->prepare($query);
        $stmt->bindValue(":name", $name, PDO::PARAM_STR );
        $stmt->bindValue(":value", $value, PDO::PARAM_STR );
        $stmt->bindValue(":major", $major, PDO::PARAM_INT );
        $stmt->bindValue(":id", $id, PDO::PARAM_INT );

        $stmt->execute();

    }


        $query = '  DELETE FROM '.DB_TEMPLATES.'
                    WHERE
                    '.DB_TEMPLATES_ID.'=:id';

        $stmt = $this->pdo->prepare($query);
        $stmt->bindValue(":id", $id, PDO::PARAM_INT);

        $stmt->execute();


    }

    (snip - irrelevant functions)

}

At certain points along the way, I had thought this may have been a character encoding issue, but I don't think that any more. Perhaps it is some odd character escaping issue? Other posts here on SO seem to indicate it is a configuration issue / (feature?) of apache's mod_security. I tried disabling a few different things but that did not work, and now I am just at a loss as for what to think.

Thanks for your help.

Upvotes: 1

Views: 2109

Answers (2)

santiago arizti
santiago arizti

Reputation: 4737

I had the same issue and I will tell you how I debugged it and fixed it.

In my case I could reproduce the forbidden response by writting some very specific inline css directives (inside a style attribute in an html tag).

I analized the error log file of apache (in my case it is at /usr/local/apache/logs/error_log and filtered only by the response code I was getting.

grep 406 error_log

The explanation said it was a rule by ModSecurity, this is what it said:

[Mon Jan 02 13:21:10.394376 2017] [:error] [pid 12827:tid 139784907347712] [client 255.255.255.255] ModSecurity: Access denied with code 406 (phase 2). Pattern match "(?: (?:height|width) ?(?:=|\\\\:) ?[0-9] ?px|overflow ?: ?(?:auto|hidden)|style ?= ?\\"? ?display ?: ?none ?)" at ARGS:body. [file "/usr/local/apache/conf/modsec2/30_asl_antispam.conf"] [line "174"] [id "300076"] [rev "29"] [msg "Atomicorp.com WAF AntiSpam Rules: Hidden Text Detected"] [data " height:8px"] [severity "CRITICAL"] [hostname "my.website.mx"] [uri "/index.php"] [unique_id "WGqoJqwQ25oAADIbgkcAAAGP"]

I investigated how to disable this and it turns out you can add this in a conf file:

<Location "/">
    SecRuleRemoveById 300076
</Location>

Problem solved for me. Note, if you want to disable more than one rule you can separate the ids by spaces.

Upvotes: 0

regilero
regilero

Reputation: 30496

The fact that you have a 406, and also the fact that this is based on the presence of the < before the word script, leads directly to a security filter in action. So mod_security and apache configuration files would be more useful than the js/php code to find the root blocking rule.

This looks like an anti-xss filter. Maybe a very simple one like:

SecFilter "<( |\n)*script"

If you can alter this mod_security configuration, then do that... or don't. It's about security. Maybe you should keep theses filter in place to protect your application for really unwanted script injections. Maybe the real solution is that your official application forms should manage communications with the server without being suspicious for mod_security. And here your POST is too much like a Cross Site Scripting attack for a tool which is only reading the stream and detect a javascript call inside.

You could for example add a base64 encoding on the POSTed value (from the js) and decode the result on the PHP side, some base64 encoding js libraries are available on github.

You could in fact apply any transformation on your data that render this data HTML-free (and Javascript-free), at least for the HTTP filtering tools. And Base64 is usually the solution, but check how the used library manage encoding of utf-8 characters, the real base64 was ascii only.

Upvotes: 3

Related Questions