0

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") ;
}
ELA
  • 11
  • 5
  • 2
    Golden rule is.. **never trust the client**. Do your security/validation/authentication on the server-side – mhodges Mar 22 '20 at 18:59
  • 1
    Validation is a must on Front-End and the Back-End, because someone may bypass the Front-End validation by using something like postman, so the server must validate what it receives. This is a must – Ahmed I. Elsayed Mar 22 '20 at 19:23
  • I am using PHP as back-end langauge. Can you post an example on how to protect against that on back-end ? Maybe one way is to check if the session id is equal to the user_id in the table of that image ? (To be sure that he can't view other user's images ) – ELA Mar 22 '20 at 19:25
  • You should only allow fetching of an image if the client presents a cookie - and this cookie must have the `HttpOnly` flag and be encrypted with one of the symmetric encryption algorithms (e.g. AES-256). The cookie must have a short expiration - e.g. 10-15 min – IVO GELOV Mar 22 '20 at 19:53
  • @IVOGELOV What should be the value of that cookie ? And based on what should the user be allowed to fetch an image ? – ELA Mar 22 '20 at 22:42
  • @ELA — That's rather the point. We don't know what rules you want to apply. Why should this hypothetical user be allowed to see image 219 but not image 300? You need to determine the rule and then translate it to code. So if it is a matter of checking against user id, then you need a fairly standard authentication system with a session cookie that identifies which user is logged in. – Quentin Mar 23 '20 at 09:18
  • Does this answer your question? [What are the common defenses against XSS?](https://stackoverflow.com/questions/3129899/what-are-the-common-defenses-against-xss) – Funk Forty Niner May 24 '20 at 13:13

1 Answers1

-1

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.

IVO GELOV
  • 13,496
  • 1
  • 17
  • 26