Search code examples
phpvariablessessionisset

Can i use empty() for a Undefined Variable?


index.php:

if(isset($_SESSION['user_id'])){
  $user_id = $_SESSION['user_id'];
}
###$_SESSION['user_id'] is = to $user['user_id'] that i get from database in a select query on login.php

As you can see above, i set the variable $user_id only when the user is loge in. I use if(empty($user_id)){} to make some conditons and it's working well, but i'm curious, because if the user is not logged in $user_id will be a Undefined Variable, i'm new to php, so i want to know if i'm doing it correctly.

On a page that the user should not view unless he is logged in i have added this code:

if(empty($user_id)) {
  $_SESSION['message'] = "You need to loge in to view this page";
  header("location: verify/error.php");
  exit;    
}

And to display some content related to the user i use this code:

if(!empty($user_id)){
   echo 'content related to the user, divs...etc';
}

I have two questions:

1 - Am i doing it correctly?

2 - Should i change my code to:

if(isset($_SESSION['user_id'])){
  $user_id = $_SESSION['user_id'];
}else{
  $user_id = '';
}

Solution

  • Am I doing it correctly?

    First and foremost, my answer isn't going to describe the implementation of using empty() in your case against undefined variable warnings. This answer is to give you direction, and show you a bit of OOP for your case and why its efficient.

    Take, as an example, page A doesn't require a user to be authenticated for read access but page B does.

    Rather than re-writing this check, consistently throughout your application for page 'n', we can write a class structure that does it for you depending on the access scope.

    Lets start of by making the application know when to disallow read access by creating an interface. This will eventually be used to extend the User authentication class whenever we want to revoke read access when a user is not logged in.

    namespace Application\Auth;
    
    interface MustBeLoggedIn
    {
    }
    

    Next, we can go ahead and build the authentication class. This is what holds the directive of how, or what order, this class will execute its methods depending on its instance type.

    namespace Application\Auth;
    
    class Authenticable {
    
        protected $_user_id;
        private $_csrf = 'user_id';
    
        public function __construct()
        {
            # PHP 7+
            $this->_user_id = $_SESSION[$this->_csrf] ?? '';
    
            # PHP < 5.6
            # $this->user_id = isset($_SESSION[$this->_csrf]) ? $_SESSION[$this->csrf] : '';
    
            if($this instanceof MustBeLoggedIn)
                $this->mustBeLoggedIn();
        }
    
        private function mustBeLoggedIn()
        {
            if(!isset($this->_user_id))
                $this->authError();
        }
    
        protected function authError()
        {
            exit();
        }
    
        protected function isLoggedIn()
        {
            return isset($this->_user_id);
        }
    
    }
    

    Finally, we can build two User classes. The first class will revoke read access to any unauthorised users.

    namespace Application\Auth;
    
    class User extends Authenticable implements MustBeLoggedIn
    {
        public function doSomething()
        {
            if($this->isLoggedIn())
                echo $this->_user_id;
        }
    
        protected function authError()
        {
            $_SESSION['message'] = 'Oh snap! Looks like you need to be logged in to view this page.';
            header('Location: verify/error');
            exit();
        }
    }
    

    The second class will allow both Guests and authenticated users to have read access.

    namespace Application;
    
    class User extends Authenticable
    {
        public function doSomething()
        {
            if($this->isLoggedIn())
                echo $this->_user_id;
    
            if(!$this->isLoggedIn())
                echo 'Well, you can still view this page';
        }
    }
    

    Deploying a view is now easy. Without having to constantly re-declare whether the value exists, and decide what to do, you can instance whichever suits your scope.

    If we want to give the user read access when he is logged in or logged out, we can use Application\User.

    class MyView extends \Application\User {
        public function sayHi() {
            echo $this->isLoggedIn() ? "Hi, {$this->user_id}!" : "Hi, Guest!";
        }
    }
    
    (new MyView())->sayHi();
    

    If we want to revoke read access to unauthorised users, we can use Application\Auth\User.

    class MyView extends \Application\Auth\User {
        public function sayHi() {
            echo "Hi, {$this->user_id}!";
        }
    }
    
    (new MyView())->sayHi(); # Guests will be redirected to error/verify
    

    I hope by showing you this, it might help you understand a bit more about PHP. Also, things to note is that you're likely vulnerable to CSRF attacks because an ID (assuming representing an integer) is extremely easy to forge. Perhaps look at using a JSON Web Token and pass the user ID into it.

    $_SESSION['csrf'] = \Firebase\JWT\JWT::encode(array('user_id' => 1), 'secret'); # An integer represented a base-64 encoding is going to stand out, perhaps store a generated unique token or something else in regards to identifying this user
    $user = \Firebase\JWT\JWT::decode($_SESSION['csrf'], 'secret', array('HS256')); # $user['user_id'] would hold 1
    

    Touching on fyrye's comment regarding exploitation within the usage of JWT, the biggest point to take away is HS256 security majority deriving from the secret. What does that mean? It means you are in control of how secure you make it. The method I use is to think of a secure password, but something to do with the JWT.

    $secret = '_userAuthentication%App';
    

    Then, to increase the security of this, I hash the password before I provide it to the HS256 for usage of encryption.

    use \Firebase\JWT\JWT;
    
    JWT::encode(array(
        'Data' => 'You want to secure from the user',
        'Integrity' => 'This STILL should not mean it is trusted data'
    ), password_hash($secret, PASSWORD_BCRYPT));
    

    When decoding the JWT, it is also important to remember the basics - never trust data. We can make use of the try catch finally blocks, disregarding finally in this case, to ensure that we're getting the correct information back.

    use \Firebase\JWT\JWT;
    
    try {
        $csrf = JWT::decode($jwt, password_hash($secret, PASSWORD_BCRYPT), array('HS256'));
    } catch (Exception $e) {
        // Data was invalid - Potential forge ?
    }
    

    Final note, if you're using SSL - which these days is a requirement, then sign the JWT with RSA.