security: 每人一把口令(口令即身份) + 随机会话token + 无配置拒绝启动 + 爆破节流

- STORY_WEB_PASSWORD(默认story) 废弃 → STORY_WEB_USERS=名字1:口令1,名字2:口令2;
  未配置/口令<8位/口令或用户名重复 → 启动即退出,杜绝弱默认口令裸奔
- cookie 不再存口令原文:登录发 secrets.token_urlsafe(32) 随机token,
  会话存 SQLite sessions 表(30天);登出删token;从 USERS 移除某人=吊销其全部会话
- updated_by 改由服务端按会话身份填写,前端自报 by 不再可信;登录框去掉昵称字段
- 登录失败全局递增节流(最多sleep 5s),口令比较用 secrets.compare_digest
- Dockerfile/compose 移除一切口令默认值;compose 未设 STORY_WEB_USERS 直接报错
- 顺手修 playtest.js 走位/动画/out_ref 行未转义的存储型XSS(esc补齐)
This commit is contained in:
2026-06-10 17:34:50 +08:00
parent 0f42fa13f1
commit 90402c4a17
9 changed files with 152 additions and 34 deletions

View File

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""M5 Web 编辑器的 SQLite 存储层。
表 eventsgroup(PK)/title/theme/status/ir_json/updated_at/updated_by/notes。
表 eventsgroup(PK)/title/theme/status/ir_json/updated_at/updated_by/notes。
末次写入生效(设计接受),不做锁。
表 sessionstoken(PK)/user/expires_at——登录会话cookie 只存随机 token不存口令
"""
import json
import os
@ -38,6 +39,43 @@ def init_db(path=None):
notes TEXT
)"""
)
c.execute(
"""CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user TEXT NOT NULL,
expires_at REAL NOT NULL
)"""
)
# ---------- 会话 ----------
def create_session(token, user, expires_at, path=None):
with _conn(path) as c:
c.execute("INSERT OR REPLACE INTO sessions (token, user, expires_at) "
"VALUES (?,?,?)", (token, user, expires_at))
def get_session_user(token, now, path=None):
"""token 有效返回用户名;过期则顺手删除并返回 None。"""
with _conn(path) as c:
r = c.execute("SELECT user, expires_at FROM sessions WHERE token=?",
(token,)).fetchone()
if not r:
return None
if r["expires_at"] < now:
c.execute("DELETE FROM sessions WHERE token=?", (token,))
return None
return r["user"]
def delete_session(token, path=None):
with _conn(path) as c:
c.execute("DELETE FROM sessions WHERE token=?", (token,))
def purge_sessions(now, path=None):
with _conn(path) as c:
c.execute("DELETE FROM sessions WHERE expires_at < ?", (now,))
def list_events(status=None, path=None):