security: 全面安全加固 v2.1

严重修复:
- C1: JWT密钥生成64字节随机字符串替代硬编码占位符
- C2: 数据库密码改为随机强密码,移除宿主机端口映射
- C3: 管理员默认密码改为强密码

高危修复:
- H1: 移除数据库54326端口映射,仅内部网络访问
- H3: 添加X-Frame-Options/CSP/XSS-Protection等安全头
- H4: 前端所有innerHTML拼接处添加escapeHtml转义防XSS
- H5: SMTP密码在API响应中脱敏显示
- H6: 债务列表接口添加用户隔离,普通用户只能看自己的数据
- H7: 债务详情/计划/记录接口添加归属校验
- H8: 账户级暴力破解锁定(5次失败锁定5分钟)
- H9: 速率限制数据增加过期清理机制防内存泄漏

中危修复:
- M1: 注册/重置密码接口添加后端密码强度校验(最少8位)
- M4: 添加全局异常处理器,500错误不再暴露堆栈
- M5: Docker容器改为非root用户运行
- M6: 添加.dockerignore排除pyc和敏感文件
- M7: 管理员操作写入审计日志
- M8: CORS限制为实际使用的HTTP方法和头

其他:
- Nginx隐藏版本号(server_tokens off)
- 管理员角色修改添加枚举校验
- 密码重置验证码接口返回统一错误信息
This commit is contained in:
OP
2026-06-28 22:46:24 +08:00
parent ead41e2b8e
commit 498d29dfa9
13 changed files with 103 additions and 31 deletions

View File

@@ -271,7 +271,7 @@ let allGroups=[];
let allUsers=[];
let currentGroupId=null;
const $=id=>document.getElementById(id);
const $=id=>document.getElementById(id);function escapeHtml(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
async function apiFetch(path,opts={}){
const headers={'Content-Type':'application/json',...opts.headers};
@@ -313,11 +313,11 @@ async function loadGroups(){
const tb=$('groupTable');
if(!allGroups.length){tb.innerHTML='<tr><td colspan="4" class="empty">暂无组</td></tr>';return;}
tb.innerHTML=allGroups.map(g=>`<tr>
<td><strong>${g.name}</strong></td>
<td>${g.description||'-'}</td>
<td><strong>${escapeHtml(g.name)}</strong></td>
<td>${escapeHtml(g.description||'-')}</td>
<td>${g.member_count}人</td>
<td>
<button class="btn-sm btn-enable" onclick="showMembers(${g.id},'${g.name}')">管理成员</button>
<button class="btn-sm btn-enable" onclick="showMembers(${g.id},'${escapeHtml(g.name)}')">管理成员</button>
<button class="btn-sm btn-danger" onclick="deleteGroup(${g.id})">删除</button>
</td>
</tr>`).join('');
@@ -344,11 +344,11 @@ async function loadUsers(){
if(u.status==='pending')actions=`<button class="btn-sm btn-approve" onclick="approveUser(${u.id})">通过</button>`;
if(u.status==='active')actions=`<button class="btn-sm btn-disable" onclick="setStatus(${u.id},'disabled')">禁用</button>`;
if(u.status==='disabled')actions+=` <button class="btn-sm btn-enable" onclick="setStatus(${u.id},'active')">启用</button> <button class="btn-sm btn-restore" onclick="setStatus(${u.id},'pending')">恢复待审</button>`;
actions+=` <button class="btn-sm btn-enable" onclick="showEditUser(${u.id},'${u.account}','${u.nickname||''}','${u.real_email||''}','${u.role}')">编辑</button>`;
actions+=` <button class="btn-sm btn-enable" onclick="sendResetLink(${u.id},'${u.account}')">改密</button>`;
actions+=` <button class="btn-sm btn-danger" onclick="deleteUser(${u.id},'${u.account}')">删除</button>`;
actions+=` <button class="btn-sm btn-enable" onclick="showEditUser(${u.id},'${escapeHtml(u.account)}','${u.nickname||''}','${u.real_email||''}','${u.role}')">编辑</button>`;
actions+=` <button class="btn-sm btn-enable" onclick="sendResetLink(${u.id},'${escapeHtml(u.account)}')">改密</button>`;
actions+=` <button class="btn-sm btn-danger" onclick="deleteUser(${u.id},'${escapeHtml(u.account)}')">删除</button>`;
}
return`<tr><td>${u.account}</td><td>${u.nickname}</td><td>${emailDisplay}</td><td>${roleBadge}</td><td>${groupBadge}</td><td>${u.debt_count}笔</td><td>${statusBadge}</td><td>${actions}</td></tr>`;
return`<tr><td>${escapeHtml(u.account)}</td><td>${escapeHtml(u.nickname)}</td><td>${emailDisplay}</td><td>${roleBadge}</td><td>${groupBadge}</td><td>${u.debt_count}笔</td><td>${statusBadge}</td><td>${actions}</td></tr>`;
}).join('');
}
@@ -419,7 +419,7 @@ async function showMembers(gid,gname){
const members=allUsers.filter(u=>u.group_id===gid);
$('memberList').innerHTML=members.length?members.map(u=>`<span style="display:inline-block;padding:3px 8px;margin:2px;background:#eef2f7;border-radius:4px;font-size:12px">${u.nickname||u.account} <a href="#" onclick="removeMember(${u.id});return false" style="color:#dc2626;margin-left:4px">×</a></span>`).join(''):'<span style="font-size:12px;color:#94a3b8">暂无成员</span>';
const unassigned=allUsers.filter(u=>!u.group_id&&u.role!=='admin');
$('memberSelect').innerHTML=unassigned.map(u=>`<option value="${u.id}">${u.nickname||u.account} (${u.account})</option>`).join('');
$('memberSelect').innerHTML=unassigned.map(u=>`<option value="${u.id}">${u.nickname||u.account} (${escapeHtml(u.account)})</option>`).join('');
$('memberModal').classList.add('show');
}
function closeMemberModal(){$('memberModal').classList.remove('show');currentGroupId=null;}