1

I am building a small app for Nextcloud (16). I have to execute some code in an external PHP file, not in the Nextcloud app. This page has to be secured against unauthorized access. I want to achieve this with the existing Nextcloud session cookie.

Currently, I read the nc_session_id cookie from the user's browser and check if this session exists in PHP's session path. This should be secure because an attacker can usually not guess the id.

This is what the Nextcloud session in the browser looks like:

Nextcloud cookies

I have tried to check the cookie with session_status('nc_session_id') != PHP_SESSION_NONE but this always returns int(1) --> the session does not exist, because I would have to run session_start() before that. BUT in this special case, the external page shall never start a new session itself - it should only check if a valid Nextcloud session already exists.

My current code seems to do the job:

session_name('nc_session_id');
$sessid_cook = filter_input(INPUT_COOKIE, "nc_session_id", FILTER_SANITIZE_STRING);
$sess_path = session_save_path().'/sess_'.$sessid_cook;

if(isset($_COOKIE['nc_session_id']) &&
    isset($_COOKIE['nc_username']) && 
    file_exists($sess_path)) {
    echo "Session okay";
    session_start();
} else {
    echo "Access denied";
    exit;
}

// my protected logic here

If I manipulate the session cookie in my browser, the PHP code on the server can not find the session file for that manipulated cookie. So the access is denied.

This works in my current setup, but what happens if the sessions are handled by Redis or Memcache? The cookies could not be checked locally.

Is there some better way to "validate" session cookies before starting a PHP session?

Is my solution secure or does it have some flaws?

Olaf Kock
  • 46,930
  • 8
  • 59
  • 90
Dexter2k
  • 11
  • 3
  • FWIW, it's not a good idea to assume sessions are stored in files on your disk. PHP sessions are an abstract thing, they could, depending on the setup, be stored in a DB, memcache, other server's disk or any other storage mechanism. If you need to look for session "files", you're probably doing something very wrong. – shevron Oct 07 '19 at 14:44

1 Answers1

2

You cannot assume that your users are correctly connected simply by the existence of the cookie "nc_session_id" because the content of this cookie is always identical to the content of the cookie named with the value "instanceid" in your nextcloud configuration.

To do this, you must decrypt the content of the nextcloud session and test different values

<?php
$session_path = session_save_path().'/sess_'.$_COOKIE["nc_session_id"];
$passphrase = $_COOKIE["oc_sessionPassphrase"];
$nextcloud_path = '/var/www/nextcloud';

include $nextcloud_path.'/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Base.php';
include $nextcloud_path.'/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Rijndael.php';
include $nextcloud_path.'/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/AES.php';
include $nextcloud_path.'/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Hash.php';

use phpseclib\Crypt\AES;
use phpseclib\Crypt\Hash;

class nextcloudSession {
    private $cipher;
    private $session_path;
    private $passphrase;

    public function __construct($session_path,$passphrase) {
        $this->cipher = new AES();
        $this->session_path = $session_path;
        $this->passphrase = $passphrase;

    }

    public function getSession() {
        $session_crypted = file_get_contents($this->session_path);
        $session_crypted_content = substr($session_crypted,strpos($session_crypted,'"')+1,-2);
        $session = json_decode($this->decrypt($session_crypted_content,$this->passphrase), true);
        return $session;
    }

    public function setSession($session) {
        $session_crypted_content = $crypt->encrypt(json_encode($session),$this->passphrase);
        $session_crypted = 'encrypted_session_data|s:'.strlen($session_crypted_content).':"'.$session_crypted_content.'";';
        return file_put_contents($session_path,$session_crypted);
    }

    public function isLogged() {
        $session = $this->getSession();
        if (isset($session["login_credentials"]) and (!isset($session["two_factor_auth_uid"]) and isset($session["two_factor_auth_passed"])) and !isset($session["app_password"])) {
            return true;
        } else {
            return false;
        }
    }
    private function calculateHMAC(string $message, string $password = ''): string {

        $password = hash('sha512', $password . 'a');

        $hash = new Hash('sha512');
        $hash->setKey($password);
        return $hash->hash($message);
    }
    private function encrypt(string $plaintext, string $password = ''): string {

        $this->cipher->setPassword($password);

        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $charactersLength = strlen($characters);
        $iv = '';
        for ($i = 0; $i < 16; $i++) {
            $iv .= $characters[rand(0, $charactersLength - 1)];
        }

        $this->cipher->setIV($iv);

        $ciphertext = bin2hex($this->cipher->encrypt($plaintext));
        $hmac = bin2hex($this->calculateHMAC($ciphertext.$iv, $password));

        return $ciphertext.'|'.$iv.'|'.$hmac;
    }
    private function decrypt(string $authenticatedCiphertext, string $password = ''): string {

        $this->cipher->setPassword($password);

        $parts = explode('|', $authenticatedCiphertext);
        if (\count($parts) !== 3) {
            return false;
            throw new \Exception('Authenticated ciphertext could not be decoded.');
        }

        $ciphertext = hex2bin($parts[0]);
        $iv = $parts[1];
        $hmac = hex2bin($parts[2]);

        $this->cipher->setIV($iv);

        if (!hash_equals($this->calculateHMAC($parts[0] . $parts[1], $password), $hmac)) {
            return false;
            throw new \Exception('HMAC does not match.');
        }

        $result = $this->cipher->decrypt($ciphertext);
        if ($result === false) {
            return false;
            throw new \Exception('Decryption failed');
        }

        return $result;
    }

}

$nc_session = new nextcloudSession($session_path,$passphrase);
$_SESSION = $nc_session->getSession();
$isLogged = $nc_session->isLogged();
$nc_session->setSession($_SESSION);

Tip: you can retrieve the login and password of the nextcloud session, I use it for a homemade SSO solution with the nginx reverseproxy and a configuration with 'auth_request'.

Pocket
  • 31
  • 4
  • Hi @Pocket, this is very helpful, I am trying to understand the cookie business in nextcloud and how they handle log in! Could you point me to a resource? [I would like to unterstand how to correctly interact using curl and cookies](https://stackoverflow.com/questions/64704263/generate-login-cookie-in-shell-using-curl) – the.polo Nov 09 '20 at 12:58