首页 > 解决方案 > 如何在外部 PHP 页面中安全地从 Nextcloud 继续会话

问题描述

我正在为 Nextcloud (16) 构建一个小应用程序。我必须在外部 PHP 文件中执行一些代码,而不是在 Nextcloud 应用程序中。此页面必须防止未经授权的访问。我想用现有的 Nextcloud 会话 cookie 来实现这一点。

目前,我nc_session_id从用户的浏览器读取 cookie 并检查 PHP 的会话路径中是否存在该会话。这应该是安全的,因为攻击者通常无法猜测 id。

这是浏览器中 Nextcloud 会话的样子:

Nextcloud 饼干

我试图检查 cookie session_status('nc_session_id') != PHP_SESSION_NONE 但这总是返回 int(1) --> 会话不存在,因为我必须在此之前运行 session_start() 。但是在这种特殊情况下,外部页面本身永远不会启动新会话 - 它应该只检查有效的 Nextcloud 会话是否已经存在。

我当前的代码似乎可以完成这项工作:

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

如果我在浏览器中操作会话 cookie,则服务器上的 PHP 代码找不到该操作 cookie 的会话文件。所以访问被拒绝。

这适用于我当前的设置,但如果会话由 Redis 或 Memcache 处理会发生什么?无法在本地检查 cookie。

在开始 PHP 会话之前是否有更好的方法来“验证”会话 cookie?

我的解决方案是安全的还是有一些缺陷?

标签: phpsessionsession-cookiesnextcloud

解决方案


您不能仅仅通过 cookie“nc_session_id”的存在就假设您的用户已正确连接,因为此 cookie 的内容始终与 nextcloud 配置中以值“instanceid”命名的 cookie 的内容相同。

为此,您必须解密 nextcloud 会话的内容并测试不同的值

<?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);

提示:您可以检索 nextcloud 会话的登录名和密码,我将其用于使用 nginx reverseproxy 和配置 'auth_request' 的自制 SSO 解决方案。


推荐阅读