How to protect AJAX or javascript web application

105 Views Asked by At

This is a simple function that use AJAX and get information about an image in the database with id=219 when a button is clicked

Anyone loading this webpage can change the javascript code by going to the source code. Then by clicking the button he will run the modified code (like changing image_id from 219 to 300). So he can get information about any image just by changing image_id

The question is how to protect against that client-side attack or XSS ?

function clicked () {
    var xhttp = new XMLHttpRequest () ;

    xhttp.onreadystatechange = function () {
        if (this.readyState == 4 && this.status == 200){
            var obj = JSON.parse (this.responseText);
            alert (obj.description);
        }
    };

    xhttp.open ("POST","get_title_description.php", true);
    xhttp.setRequestHeader ("Content-type", "application/x-www-form-urlencoded");
    xhttp.send ("image_id=219") ;
}
1

There are 1 best solutions below

0
On

You can use something like this for generating and validating the cookie:

define('COOKIE_TOKEN', 'my_token');

class BaseAuth
{
  protected $uid;

  private static function base64url_encode(string $s): string
  {
    return strtr($s,'+/=','-|_');
  }

  private static function base64url_decode(string $s): string
  {
    return strtr($s,'-|_','+/=');
  }

  // Encodes after encryption to ensure encrypted characters are URL-safe
  protected function token_encode(String $string): string
  {
    $iv_size = openssl_cipher_iv_length(TYPE_CRYPT);
    $iv = openssl_random_pseudo_bytes($iv_size);

    $encrypted_string = @openssl_encrypt($string, TYPE_CRYPT, SALT, 0, $iv);

    // Return initialization vector + encrypted string
    // We'll need the $iv when decoding.
    return self::base64url_encode($encrypted_string).'!'.self::base64url_encode(base64_encode($iv));
  }

  // Decodes from URL-safe before decryption
  protected function token_decode(String $string): string
  {
    // Extract the initialization vector from the encrypted string.
    list($encrypted_string, $iv) = explode('!', $string);
    $string = @openssl_decrypt(self::base64url_decode($encrypted_string), TYPE_CRYPT, SALT, 0, base64_decode(self::base64url_decode($iv)));
    return $string;
  }

  // performs log-out
  public function clear_cookie()
  {
    setcookie(COOKIE_TOKEN, '', time() - 300, '/api', '', FALSE, TRUE); // non-secure; HTTP-only
  }

  private function userIP(): string
  {
    return $_SERVER['REMOTE_ADDR'];
  }

  // validates Login token
  public function authorized(): bool
  {
    if(isset($_COOKIE[COOKIE_TOKEN]))
    {
      $stamp = time();
      $text = $this->token_decode($_COOKIE[COOKIE_TOKEN]);
      if($text != '')
      {
        $json = json_decode($text,TRUE);
        if(json_last_error() == JSON_ERROR_NONE)
        {
          if($json['at'] <= $stamp AND $json['exp'] > $stamp AND $json['ip'] == $this->userIP() AND $json['id'] != 0)
          {
            // check if user account is still active
            $res = $db->query("SELECT id,active,last_update,last_update > '".$json['last']."'::timestamptz AS expired FROM account WHERE id = ".$json['id']);
            $info = $db->fetch_assoc($res);
            if($info['active'] != 0)
            {
              if($info['expired'] == 0)
              {
                // extend the token lifetime
                $this->sendToken($info);
                $this->uid = $json['id'];
                return TRUE;
              }
            }
          }
        }
      }
      $this->clear_cookie();
    }
    return FALSE;
  }

  public function login(String $username, String $password): bool
  {
    $stm = $db-prepare("SELECT id,user_name AS username,user_pass,full_name,active,last_update,COALESCE(blocked_until,NOW()) > NOW() AS blocked 
      FROM account WHERE user_name = :user");
    $res = $stm->execute(array('user' => strtolower($json['username'])));
    if($res->rowCount())
    {
      $info = $db->fetch_assoc($res);
      if($info['active'] == 0)
      {
        // Account is disabled
        return FALSE;
      }
      elseif($info['blocked'] != 0)
      {
        // Blocked for 5 minutes - too many wrong passwords
        // extend the blocking
        $db->query("UPDATE account SET blocked_until = NOW() + INTERVAL 5 minute WHERE id = ".$info['id']);
        return FALSE;
      }
      elseif(!password_verify($password, $info['user_pass']))
      {
        // Wrong password OR username
        // block account
        $db->query("UPDATE account SET blocked_until = NOW() + INTERVAL 5 minute WHERE id = ".$info['id']);
        return FALSE;
      }
      else
      {
        unset($info['user_pass']);
        unset($info['blocked']);
        $this->sendToken($info);
        return TRUE;
      }
    }
  }
}

If you do not need to authenticate and authorize your users and just need random unpredictable image IDs - you can simply use UUIDs.