faet: 修改README.md,提升安全级别意识.
This commit is contained in:
1 parent
06b418ec51
commit
face59cdd7
5 files changed
+67
-13
No files matched your search
@@ -11,7 +11,7 @@
|
||||
|
||||
[**Demo演示**](https://2fa.nezha.pp.ua) https://2fa.nezha.pp.ua
|
||||
|
||||
- 演示站默认允许任何用户登录,区别于正常部署只有白名单用户才允许登录
|
||||
- 演示站配置了 `OAUTH_ALLOW_ALL=true`变量, 默认允许任何用户登录,正常部署切记 **不要配置OAUTH_ALLOW_ALL参数** | **不要配置OAUTH_ALLOW_ALL参数** | **不要配置OAUTH_ALLOW_ALL参数**
|
||||
- 演示站数据可以任意修改、删除
|
||||
|
||||
---
|
||||
@@ -107,18 +107,23 @@ curl "https://api.telegram.org/bot<你的BotToken>/setWebhook?url=https://<你
|
||||
| `CLOUDFLARE_D1_DATABASE_ID` | D1 数据库 ID | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
|
||||
|
||||
#### 🔐 必填:系统安全参数
|
||||
|
||||
> ⚠️ **高危警告:关于 ENCRYPTION_KEY 的修改风险**
|
||||
> `ENCRYPTION_KEY` 是用于加密数据库中 2FA 核心令牌种子的底层主密钥。**一旦系统投入生产使用并存入了数据,请绝对不要修改此环境变量的值!** 修改后将导致之前存入的所有数据瞬间无法解密,彻底变成死数据。
|
||||
|
||||
| Secret Name | 说明 | 示例值 |
|
||||
| :--- | :--- | :--- |
|
||||
| `OAUTH_ALLOWED_USERS` | 允许登录的账户白名单!防止陌生人登录。 | `me@example.com,github_user` (逗号分隔) |
|
||||
| `JWT_SECRET` | 签发登录状态的系统密钥 | `random_string_here`,越长越好,建议32位 |
|
||||
| `ENCRYPTION_KEY` | 对数据库中关键数据底层加密密钥 | 建议使用 `openssl rand -hex 16` 生成 |
|
||||
| `OAUTH_ALLOWED_USERS` | 允许登录的账户白名单 | `me@example.com,github_user` (逗号分隔) |
|
||||
| `OAUTH_ALLOW_ALL` | 无视白名单放行所有人(**仅限测试**) | `true` 或 `1` (生产环境切勿配置) |
|
||||
| `JWT_SECRET` | 签发会话状态的系统密钥 | 越长越好, 建议 32 位随机字符 |
|
||||
| `ENCRYPTION_KEY` | 对数据库持久化2FA核心密码集的唯一底层加密密钥 | **必填**。建议使用 `openssl rand -hex 32` 生成|
|
||||
|
||||
#### 🔑 必填:Cloudflare D1数据库参数
|
||||
|
||||
| Secret Name | 说明 | 示例值 |
|
||||
| :--- | :--- | :--- |
|
||||
| `CLOUDFLARE_D1_DATABASE_ID` | Cloudflare D1 数据库 ID | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
|
||||
| `ENCRYPTION_KEY` | Cloudflare D1 数据库名称 | 默认为 `2fauth-db` |
|
||||
| `CLOUDFLARE_D1_DATABASE_NAME` | Cloudflare D1 数据库名称 | 默认为 `2fauth-db` |
|
||||
|
||||
#### 🔑 必填:OAuth 身份信息 (按需选择对应的组合)
|
||||
> 如果你用 GitHub 登录,就填下面三个;如果你用 Google,就填 Google 的那三个。
|
||||
@@ -169,5 +174,16 @@ cd backend && npm run dev
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ 安全性声明 (Security Policy)
|
||||
|
||||
本项目涉及高度敏感的 2FA 认证种子数据的云端存储,在架构安全方面我们做出了如下强制约束:
|
||||
|
||||
1. **密钥隔离设计**:`JWT_SECRET` (用于签发 Session/Device Token) 与 `ENCRYPTION_KEY` (用于静态加密数据库的核心密码) 必须从 Cloudflare 环境变量层面强制分离,不要设置相同变量值。任何 `ENCRYPTION_KEY` 缺失导致的系统空缺都会被高危拦截拒绝启动,从而遏制降级攻击。
|
||||
2. **零容忍 XSS (跨站脚本) 防御策略**:本项目的前端严格禁止任何未经深度转义的动态 HTML(`v-html` 或等效方法)直接上树。任何第三方代码引入必须经过人工审计。
|
||||
|
||||
遇到任何安全性相关的 Issue,请在提交时标记为 [Security] 以获得最高优的处理优先级。
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
本项目基于 [MIT License](LICENSE) 协议完全开源。你可以自由地二次开发、分发或用于商业场景,只要包含原作者和版权声明即可。
|
||||
本项目基于 [MIT License](LICENSE) 协议完全开源。你可以自由地二次开发、分发或商业使用,只要包含原作者和版权声明即可。
|
||||
@@ -71,6 +71,7 @@ export type EnvBindings = {
|
||||
OAUTH_TELEGRAM_BOT_NAME?: string;
|
||||
OAUTH_TELEGRAM_BOT_TOKEN?: string;
|
||||
OAUTH_ALLOWED_USERS: string; // 允许登录的 Email 或 Username 白名单 (必填)
|
||||
OAUTH_ALLOW_ALL?: string; // 是否允许所有用户登录 (仅推荐用于测试演示环境)
|
||||
JWT_SECRET: string;
|
||||
ENCRYPTION_KEY: string;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Hono } from 'hono';
|
||||
import { setCookie, deleteCookie } from 'hono/cookie';
|
||||
import { setCookie, getCookie, deleteCookie } from 'hono/cookie';
|
||||
import { EnvBindings, AppError } from '@/app/config';
|
||||
import { authMiddleware } from '@/shared/middleware/auth';
|
||||
import { getAvailableProviders } from '@/features/auth/providers/index';
|
||||
@@ -33,6 +33,16 @@ auth.get('/authorize/:provider', async (c) => {
|
||||
// 生成包含唯一 state 防御 CSRF 的跳转地址
|
||||
const authData = await service.generateAuthorizeUrl(providerName);
|
||||
|
||||
// 将 state 存入服务端的 HttpOnly Cookie,有效期 10 分钟
|
||||
const stateCookieName = `oauth_state_${providerName}`;
|
||||
setCookie(c, stateCookieName, authData.state, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'Lax', // 允许从第三方回调跳回时携带
|
||||
maxAge: 10 * 60, // 10分钟有效期
|
||||
path: '/',
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
authUrl: authData.url,
|
||||
@@ -45,6 +55,20 @@ auth.get('/authorize/:provider', async (c) => {
|
||||
auth.post('/callback/:provider', async (c) => {
|
||||
const providerName = c.req.param('provider');
|
||||
const body = await c.req.json();
|
||||
|
||||
// --- State 闭环校验开始 ---
|
||||
const stateCookieName = `oauth_state_${providerName}`;
|
||||
const serverState = getCookie(c, stateCookieName);
|
||||
const clientState = body.state; // 前端传回的回调 state 参数
|
||||
|
||||
if (!serverState || !clientState || serverState !== clientState) {
|
||||
throw new AppError('Invalid or missing OAuth state. Security validation failed.', 403);
|
||||
}
|
||||
|
||||
// 校验通过,清理一次性 State Cookie
|
||||
deleteCookie(c, stateCookieName, { path: '/', secure: true, sameSite: 'Lax' });
|
||||
// --- State 闭环校验结束 ---
|
||||
|
||||
const service = getService(c);
|
||||
|
||||
// 调用 Service 层处理登录
|
||||
|
||||
@@ -80,14 +80,19 @@ export class AuthService {
|
||||
* 白名单校验
|
||||
*/
|
||||
private verifyWhitelist(userInfo: OAuthUserInfo, whitelistFields: string[]) {
|
||||
// 如果 explicitly allowed all, 则放行
|
||||
const allowAllStr = String(this.env.OAUTH_ALLOW_ALL || '').toLowerCase();
|
||||
if (allowAllStr === 'true' || allowAllStr === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedUsersStr = this.env.OAUTH_ALLOWED_USERS || '';
|
||||
const allowedIdentities = allowedUsersStr.split(',').map((e: string) => e.trim().toLowerCase()).filter(Boolean);
|
||||
|
||||
const userEmail = (userInfo.email || '').toLowerCase();
|
||||
const userName = (userInfo.username || '').toLowerCase();
|
||||
|
||||
// 仅在配置了白名单且 OAUTH_ALLOWED_USERS 开发用的特定值不同时校验
|
||||
if (allowedIdentities.length > 0 && this.env.OAUTH_ALLOWED_USERS !== this.env.JWT_SECRET) {
|
||||
// 如果配置了白名单,严格执行
|
||||
if (allowedIdentities.length > 0) {
|
||||
const userEmail = (userInfo.email || '').toLowerCase();
|
||||
const userName = (userInfo.username || '').toLowerCase();
|
||||
let isAllowed = false;
|
||||
|
||||
if (whitelistFields.includes('email') && userEmail && allowedIdentities.includes(userEmail)) {
|
||||
@@ -101,6 +106,9 @@ export class AuthService {
|
||||
if (!isAllowed) {
|
||||
throw new AppError(`Unauthorized user. Email: ${userEmail}, Username: ${userName}`, 403);
|
||||
}
|
||||
} else {
|
||||
// 默认安全策略:如果未配置白名单,未开启 ALLOW_ALL,则拒绝所有人(或者也可以记录警告,但为了安全通常应拒绝)
|
||||
throw new AppError(`No authorization whitelist configured. Login denied.`, 403);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,12 @@ export class VaultService {
|
||||
constructor(env: EnvBindings, repository: VaultRepository) {
|
||||
this.env = env;
|
||||
this.repository = repository;
|
||||
this.encryptionKey = env.ENCRYPTION_KEY || env.JWT_SECRET;
|
||||
|
||||
if (!env.ENCRYPTION_KEY) {
|
||||
throw new AppError('FATAL ERROR: ENCRYPTION_KEY is not configured.', 500);
|
||||
}
|
||||
|
||||
this.encryptionKey = env.ENCRYPTION_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user