这个管理区域足够安全吗?V2

信息安全 验证 php 会话管理 mysql
2021-08-18 01:14:13

这是对我之前的问题的回应,这个管理区域是否足够安全?.

那里有一些非常有用的答案,对此我非常感激。所以我回到绘图板上,这是我的新问题,因为我对事物的安全方面不熟悉,我不确定我是否正确理解或覆盖了所有的漏洞


我试图保护系统免受的问题是:

笔记

图 1:授权和登录脚本

授权用户的流程
我希望这个图表主要是不言自明的。但是,我将尝试澄清某些功能。

  • 检查令牌:用户登录后生成的令牌,存储在$_SESSION['auth_token']用户提交的任何表单中并随其一起发送。
  • 检查会话:如果用户拥有有效的令牌,则使用 PHPSESSID 从数据库中提取他们的详细信息(欢迎用户返回时需要该名称)。如果没有找到会话 id 或令牌无效,则他们的会话已过期,需要重新登录。
  • 再生。session:一旦用户成功登录,给他们一个新的 session id ( session_regenerate_id()) 并使用新的 session id 更新数据库。设置$_SESSION['auth_token'] = md5(mt_rand(1,10000).$username);. 用户名是唯一的,因此这应该生成一个唯一的身份验证令牌,以便他们可以提交自己的表单。我不确定我是否正确地实现了这一点 - 请参阅此处示例
  • 检查登录:只需检查 bcrypted 密码是否与 bcrypted 输入匹配,并且用户名是否匹配。
  • 失败尝试:添加这一面是为了减缓蛮力攻击;首先添加验证码,然后暂时阻止 IP 并向用户/帐户持有人发送通知电子邮件。
  • 如有必要,我将添加更多细节/如果有任何错误,请更正这些细节

实施(到目前为止):

受限页面:以要求开头authenticate.php

<?php require_once "authenticate.php"; ?>

<p>Welcome <?php echo $_SESSION["guest"]; ?>!
<a href="index.php?action=logout">Logout</a></p>

<p><a href="index.php?token=<?php echo $_SESSION["auth_token"] ?>">Try again.</a></p>

Authenticate.php:login.php如果未通过身份验证重定向到
注意:我忽略了加密(在测试时),因为bcrypt在我的 PHP 版本(5.2.17)中不可用,所以我正在寻找 SHA256 或 SHA512。

<?php
    session_start();
    // load database abstraction layer
    require_once "../../dal.php";
    unset($_SESSION["login_errors"]);

function loginError($str) {
    if (isset($_SESSION["login_errors"]))
        $_SESSION["login_errors"] = "" . $_SESSION["login_errors"] . "; " . $str;
    else
        $_SESSION["login_errors"] = $str;
}
function hasValidToken() {
    return (isset($_SESSION["auth_token"]) && isset($_GET["token"])
    && $_SESSION["auth_token"] == $_GET["token"]);
}
function hasValidSession() {
    return (session_id()? hasOpenSession() : false);
}
function hasOpenSession() {
    $sql = "SELECT * FROM tbl_store_admin WHERE php_sesskey=?;";
    $data = array(session_id());
    return (dbRowsCount($sql, $data) == 1);
}
function updateUserSession() {
    $sql = "UPDATE tbl_store_admin SET php_sesskey=? WHERE username=?";
    $data = array(session_id(), $_SESSION["uid"]);
    dbQuery($sql, $data);
    return (dbRowsAffected() == 1);
}
function hasLoggedIn() {
    if (isset($_POST["uid"]) && isset($_POST["key"])) {
        $uid = htmlspecialchars($_POST["uid"]);
        $key = htmlspecialchars($_POST["key"]);
        return (getUser($uid, $key));
    } else {
        return false;
    }
}
function wrongCredentials() {
    if (isset($_SESSION["login_attempts"])) {
        $_SESSION["login_attempts"] = $_SESSION["login_attempts"]+1;
    } else {
        $_SESSION["login_attempts"] = 1;
    }
    logout();
    loginError("Bad Credentials");
    return false;
}
function getUser($uid, $key) {
    $sql = "SELECT * FROM tbl_store_admin WHERE username=? AND keycode=? LIMIT 1;";
    $data = array($uid, $key);
    $rows = dbRowsCount($sql, $data);
    if ($rows == 1) {
        dbQuery($sql, $data);
        $user = dbFetch();
        // store data in session
        $_SESSION["uid"] = $user["username"];
        $_SESSION["guest"] = $user["nickname"];
        return true;
    } else {
        return wrongCredentials();
    }
}
function logout() {
    if (isset($_SESSION["auth_token"])) {
        unset($_SESSION["auth_token"]);
    }
    if (isset($_SESSION["uid"])) {
        $sql = "UPDATE tbl_store_admin SET php_sesskey=NULL WHERE username=?;";
        $data = array($_SESSION["uid"]);
        dbQuery($sql, $data);
        unset($_SESSION["uid"]);
    }
    if (isset($_SESSION["guest"])) {
        unset($_SESSION["guest"]);
    }
}
function hasAuthenticated() {
    if (hasValidToken()) {
        if (hasValidSession()) {
            return true;
        } else {
            logout();
            return false;
        }
    } else {
        logout();
        return hasLoggedIn();
    }
}
function regenerateSession() {
    session_regenerate_id();
    updateUserSession();
    $token = "" . mt_rand(1,10000) . $_SESSION["uid"];
    $_SESSION["auth_token"] = md5($token);
}
if (isset($_GET["action"]) && $_GET["action"]=="logout") {
    logout();
}
if (!hasAuthenticated()) {
    header('Location: login.php');
} else {
    regenerateSession();
}

?>

Login.php:一个简单的登录表单

<?php
session_start();
function getRequestURL() {
    if (isset($_SESSION['HTTP_REFERER'])) {
        return $_SESSION['HTTP_REFERER'];
    }
    return "index.php?token=".$_SESSION["auth_token"];
}
?>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html>
<head>
<meta http-equiv="X-Frame-Options" content="deny">

<title>Admin Login</title>

<style type="text/css">
.errorbox {
 position:absolute;
 top:0px;
 left:0px;
 width:100%;
 height:100px;
 background-color:#FFAAAA;
 border:1px solid #FF0000;
}
.loginbox {
 width:600px;
 height:300px;
 border:5px solid #000000;
 margin:100px auto;
 
border-radius:10px;
 -moz-border-radius:10px;
 -webkit-border-radius:10px;
 background-color:#CCCCCC;
 
box-shadow:4px 4px 2px #999999;
 -moz-box-shadow:4px 4px 2px #999999;
 -webkit-box-shadow:4px 4px 2px #999999;
}
.formbox {
 width:400px;
 height:200px;
 margin:0px auto;
}
.label {
 width:120px;
 height:40px;
 text-align:right;
 line-height:40px;
 float:left;
 font-weight:bold;
}
.textinput {
 height:40px;
 width:260px;
 font-size:30px;
 line-height:40px;
}
.submitbtn {
 height:50px;
 width:100px;
 font-size:30px;
 line-height:40px;
}
</style>
</head>

<body style="font-family: Arial;font-size:20px;">

<?php
if (isset($_SESSION["login_errors"])) {
?>
    <div class="errorbox">
        Error: <?php echo ($_SESSION["login_errors"]); ?>
    </div>
<?php
}
?>

<div class="loginbox">
<h1 align="center">Admin Area</h1>

<div class="formbox">
<form action="<?php echo getRequestURL(); ?>" method="POST">
<p><big>
<div class="label">Name:</div>&nbsp;
<input class="textinput" type="text" name="uid"/><br>
<div class="label">Password:</div>&nbsp;
<input class="textinput" type="password" name="key"/><br>
</big></p>
<p align="right"><input class="submitbtn" type="submit" value="Log In"/></p>
</form>
</div>

</div>

</body>
</html>
1个回答

全面的。 这要好得多。但我还是有一些意见——

点击劫持。您关于防止点击劫持的图表非常模糊。我假设您正在执行客户端帧分解(使用在客户端上运行的 Javascript)。请注意,这是非常容易出错的,并且大多数编写自己的 framebusting 代码的人最终都会得到一些微妙的破坏。一篇学术论文调查了使用最广泛的前 500 个站点(2010 年),发现每个使用 framebusting 的站点都以不安全的方式实施它这些攻击是微妙且不明显的,因此开发人员可能认为他们的代码很好——实际上它有缺陷。

我建议您阅读以下论文,该论文概述了存在缺陷的方法,并描述了如何正确地进行 framebusting:

工作流程执行。 我不理解您关于确保checkout.php之前不应访问的讨论review.php我不清楚您为什么将其列为安全属性,或者这会增加什么安全价值。对我来说,这听起来不像是一个安全问题。此外,您的机制可能会破坏在多个选项卡中打开站点的多个副本。

如果您担心强力浏览攻击,我建议您只需确保您在站点的所有位置(包括checkout.php页面和该页面上的所有表单目标)都使用适当的 CSRF 防御。

CSRF 防御。请注意,您需要使用 CSRF 令牌来保护所有副作用操作。如果您的网站架构正确,这意味着应该使用 CSRF 令牌来保护所有 POST 请求。

CSRF 代币。您用于mt_rand()生成 CSRF 令牌。这是不安全的,代表了不好的做法。您需要使用加密强的 PRNG(例如,从 读取/dev/urandom),以确保攻击者无法预测此值。 mt_rand()在密码学上不强。例如,请参阅glibc 的 rand 中的 rand 对于登录密钥是否安全?, 随机数生成器在密码学中安全使用的要求是什么?, 来自 /dev/urandom 的 rand 对于登录密钥是否安全?.

此外,您正在调用mt_rand(5,15),这意味着您的随机数在 5..15 范围内。换句话说,你有少于 4 位的随机性。这是完全不充分的,完全被打破了。希望这是一个错字,或者我可能误解了。

登录失败。在 10 次登录尝试失败后,您将用户封禁一个月。这可能不是一个好主意,因为它使“恶意软件”很容易将某人锁定一个月。我建议在这个网站上阅读以下问题: 为什么网站会在 3 次密码尝试失败后实施锁定?, 错误尝试后拒绝登录是否无效?, 防止暴力破解登录的适当策略?, 如何保护我的登录页面?, 为什么在表单输入失败时使用 CAPTCHA 重新验证?.

饼干。确保secure在所有 cookie 上设置了标志(这样它们只会通过 HTTPS 连接发送回来)。我建议您启用 HTTP Strict Transport Security,以防止 sslstrip 式攻击。

一般评论。 您正在构建的许多东西都是标准的。将来您可能会考虑选择一个为这些机制(例如,安全会话管理、CSRF 防御、授权检查)提供支持的 Web 应用程序开发框架。此外,我鼓励您查看 OWASP 的有关 Web 应用程序安全性的资源。它们很好地涵盖了您所询问的许多主题。