php - 如何在外部 PHP 页面中安全地从 Nextcloud 继续会话
问题描述
我正在为 Nextcloud (16) 构建一个小应用程序。我必须在外部 PHP 文件中执行一些代码,而不是在 Nextcloud 应用程序中。此页面必须防止未经授权的访问。我想用现有的 Nextcloud 会话 cookie 来实现这一点。
目前,我nc_session_id
从用户的浏览器读取 cookie 并检查 PHP 的会话路径中是否存在该会话。这应该是安全的,因为攻击者通常无法猜测 id。
这是浏览器中 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?
我的解决方案是安全的还是有一些缺陷?
解决方案
您不能仅仅通过 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 解决方案。
推荐阅读
- java - 试图在 Java Web 应用程序中实现 SQL 漏洞。Java + MySQL
- mapbox - Mapbox 是如何在加载地图之前加载此图像的?
- apache-spark - Spark-Java:如何将格式为“yyyy-MM-ddThh:mm:ss.SSS+0000”的数据集字符串列转换为格式的时间戳?
- python - AWS Lambda 返回权限被拒绝尝试从 S3 存储桶获取对象
- javascript - 为什么 Javascript 没有重定向到生产服务器上的正确路径?
- python - 通过变量访问嵌套字典
- java - ControlsFx 电子表格视图跨度
- c# - SendGrid 将类别添加到邮件
- spring - 仅针对特定 url 的带有过滤器的 Spring 安全性
- angular - 在ionic3中推送通知?