feat(安全): 对外暴露面加固 + 界面去 AI 化(v1.5.0)
界面(去 AI 味): - 大屏页清除 114 处生成器残留属性 data-page-node-id - 视觉系统改回工程控制台风格:去 radial/linear-gradient、去辉光、 去标题前彩色装饰条,改为中性灰阶 + 单一蓝色强调色;KPI 色条改状态点 - 精简各页说教式长提示;修掉 profile.html 泄漏到页面上的 Markdown 星号 - 删除登录页过时的「默认账号 admin / admin123」提示(1.4.0 起已无默认口令) 安全与隐私(按「将会被公网访问」收口): - 内部异常只回 8 位事件号,完整堆栈进服务端日志(web/api.py::_internal) - 导出文件名收敛:防响应头注入与路径穿越;manage.py passwd 补用户名校验 - 登录对不存在的账号也走一次哑哈希,抹平用户名枚举的时序差异 - /api/* 读接口限速 240 次 / 60 秒 / 账号(挡住循环调 /api/bundle) - 进程 umask 0077 + 目录 0700 / 文件 0600:对话正文与主密钥的落盘权限 - 表名与库文件路径只对管理员下发;大屏页所有数据插值转义 - --debug 只允许绑定回环地址;新增 Permissions-Policy 与 413 处理器 文档: - DEPLOYMENT 新增第十三节「安全与隐私基线」;迁移表补 1.4.0 → 1.5.0 行 - SECURITY 更新支持范围、新增「信息泄漏收敛」小节与上线检查项 - .codebuddy/ 加入 .gitignore(助手工作记忆不进仓库) 版本:1.4.0 → 1.5.0(无库结构变更,user_version 仍为 4) 验证:python tools/smoke.py → ok=264 fail=0;python tools/check_docs.py → 0 处问题
这个提交包含在:
@@ -15,7 +15,7 @@
|
||||
常用:
|
||||
python manage.py init 初始化数据库(建表 + 默认配置 + 管理员)
|
||||
python manage.py serve 启动 Web(0.0.0.0:8848,进程内含调度线程)
|
||||
python manage.py serve --port 9000 --debug 开发模式(reloader 下调度只启动一份)
|
||||
python manage.py serve --debug --host 127.0.0.1 开发模式(--debug 只允许绑回环地址)
|
||||
python manage.py collect 为**所有已启用账号**各跑一次增量采集
|
||||
python manage.py collect -u alice 只为 alice 采集
|
||||
python manage.py migrate-csv [文件] 从旧版 CSV 存档导入(默认自动探测路径)
|
||||
@@ -127,9 +127,18 @@ def cmd_init(args):
|
||||
|
||||
def cmd_serve(args):
|
||||
from workbuddy_portal import create_app
|
||||
app = create_app(start_scheduler=not args.no_scheduler)
|
||||
host = args.host or config.DEFAULT_HOST
|
||||
port = args.port or config.DEFAULT_PORT
|
||||
# --debug 会打开 Werkzeug 的交互式调试器:**任意 Python 代码执行**。
|
||||
# 它虽然有 PIN,但 PIN 会打印在控制台、且在某些部署下可由机器信息推导出来,
|
||||
# 把它绑到 0.0.0.0(默认值就是这个)等于把 RCE 直接挂上公网。
|
||||
# 这里直接拒绝:调试要么在本机回环地址上做,要么就别开调试器。
|
||||
if args.debug and host not in ("127.0.0.1", "localhost", "::1"):
|
||||
_p("[error] --debug 只能绑定回环地址(当前 --host %s)。" % host)
|
||||
_p(" Werkzeug 调试器可执行任意 Python 代码,绑到对外地址等于开放 RCE。")
|
||||
_p(" 本机调试请用:python manage.py serve --debug --host 127.0.0.1")
|
||||
return 2
|
||||
app = create_app(start_scheduler=not args.no_scheduler)
|
||||
if args.debug:
|
||||
app.run(host=host, port=port, debug=True)
|
||||
return
|
||||
@@ -420,6 +429,13 @@ def cmd_passwd(args):
|
||||
if not pwd:
|
||||
_p("[error] 新账号必须给出密码")
|
||||
return 2
|
||||
# 建号必须走与注册页同一套用户名校验。绕开它的后果不是「名字不好看」:
|
||||
# 导出文件名与下载响应头都直接拼账号名,一个带 `/`、`..`、引号或 CR/LF
|
||||
# 的名字会变成路径穿越 / 响应头注入。这里从根上堵掉。
|
||||
problem = security.username_problem(args.user)
|
||||
if problem:
|
||||
_p("[error] 用户名不合规:%s" % problem)
|
||||
return 2
|
||||
conn.execute(
|
||||
"INSERT INTO users(username,password_hash,display_name,is_admin,status,"
|
||||
" created_at) VALUES(?,?,?,?,'active',?)",
|
||||
@@ -705,6 +721,9 @@ def main():
|
||||
if not getattr(args, "func", None):
|
||||
ap.print_help()
|
||||
return 0
|
||||
# 先收紧 umask 再建目录:这样 SQLite 库、-wal/-shm、导出 CSV、备份 zip
|
||||
# 从「被创建的那一刻」就是 owner-only,不必逐个事后 chmod。
|
||||
config.harden_process()
|
||||
config.ensure_dirs()
|
||||
return args.func(args) or 0
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户