Compare commits

...

1 Commits

Author SHA1 Message Date
mac
89810942cd fix: P0 安全加固(移除硬编码弱默认 + 占位符替换修正)
- backend/db.py:db() 的 DB_PASSWORD 改用 _require_env 强制读取,删除弱默认兜底
- backend/db.py:新增 _convert_placeholders 状态机替换 ? 占位符(跳过字符串字面量),并校验占位符/参数数量,替换原 sql.replace 全局替换脆弱点
- backend/flask_app.py:secret_key 改用 _require_env 强制读取,删除可预测弱默认,防止 session 伪造
- .gitea/workflows/deploy.yml:新增 2.5 步,发布时若线上 shared/.env 缺 DB_PASSWORD/SECRET_KEY 则从 CI secrets 自动补齐(已存在则保留),确保 P0 改动上线后可正常启动
2026-07-13 14:57:41 +08:00
3 changed files with 77 additions and 4 deletions

View File

@@ -38,6 +38,28 @@ jobs:
--exclude='.gitea' \
"${CLONE_DIR}/" "${RELEASE_DIR}/"
echo "=== 2.5 Ensure .env secrets ==="
# .env 不随 git 走,线上凭据由服务器本地 shared/.env 提供。
# 若缺失则从 CI secrets 自动补齐;已存在的变量保留,避免覆盖线上配置/导致 session 失效。
mkdir -p "${DEPLOY_BASE}/shared"
SHARED_ENV="${DEPLOY_BASE}/shared/.env"
touch "${SHARED_ENV}"
ensure_env_var() {
key="$1"; val="$2"
if [ -z "$val" ]; then
echo " ! ${key} 未提供(CI secret 为空),跳过写入"
return
fi
if ! grep -q "^${key}=" "${SHARED_ENV}"; then
echo "${key}=${val}" >> "${SHARED_ENV}"
echo " + 已写入 ${key}"
else
echo " = ${key} 已存在,保留原值"
fi
}
ensure_env_var "DB_PASSWORD" "${{ secrets.OPC_DB_PASSWORD }}"
ensure_env_var "SECRET_KEY" "${{ secrets.OPC_SECRET_KEY }}"
echo "=== 3. Link shared resources ==="
mkdir -p "${RELEASE_DIR}/data"
# .env from shared dir (not in git)

View File

@@ -39,12 +39,22 @@ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
# ---------- 数据库连接 ----------
def _require_env(name):
"""强制读取必需环境变量,缺失即抛出清晰错误(禁止弱默认降级)"""
val = os.environ.get(name)
if not val:
raise RuntimeError(
f"缺少必需的环境变量 {name},请在 .env 中显式配置(关键凭据禁止硬编码兜底)"
)
return val
def db():
return mysql.connector.connect(
host=os.environ.get("DB_HOST", "127.0.0.1"),
port=int(os.environ.get("DB_PORT", "3306")),
user=os.environ.get("DB_USER", "opc"),
password=os.environ.get("DB_PASSWORD", "opc123456"),
password=_require_env("DB_PASSWORD"),
database=os.environ.get("DB_NAME", "opc"),
charset="utf8mb4",
collation="utf8mb4_unicode_ci",
@@ -55,10 +65,44 @@ def now():
return datetime.utcnow().isoformat()
def _convert_placeholders(sql):
"""将 ? 占位符转为 MySQL 的 %s,仅替换字符串字面量之外的 ?。
避免 `sql.replace('?', '%s')` 的全局替换缺陷:当 SQL 字面量或 LIKE 模式
中恰好含 `?` 时会误替换、导致参数错位。这里用状态机跳过单/双引号内的内容。
"""
out = []
in_single = in_double = False
i, n = 0, len(sql)
while i < n:
ch = sql[i]
if ch == "\\" and i + 1 < n: # 跳过转义序列
out.append(ch)
out.append(sql[i + 1])
i += 2
continue
if ch == "'" and not in_double:
in_single = not in_single
elif ch == '"' and not in_single:
in_double = not in_double
out.append("%s" if (ch == "?" and not in_single and not in_double) else ch)
i += 1
return "".join(out)
def _exec(conn, sql, args=()):
"""执行 SQL自动将 ? 转为 MySQL 的 %s"""
"""执行 SQL将 ? 占位符安全地转为 MySQL 的 %s
占位符数量与参数数量必须一致,否则立即报错,防止静默错位。
"""
converted = _convert_placeholders(sql)
n_ph = converted.count("%s")
if n_ph != len(args):
raise ValueError(
f"SQL 占位符数量({n_ph})与参数数量({len(args)})不匹配,请检查 SQL 与传参: {sql}"
)
cur = conn.cursor(dictionary=True)
cur.execute(sql.replace("?", "%s"), args)
cur.execute(converted, args)
return cur

View File

@@ -19,7 +19,14 @@ app = Flask(
template_folder=str(ROOT / "templates"),
static_folder=str(ROOT / "static"),
)
app.secret_key = os.environ.get("SECRET_KEY", "opc-dev-secret-2026")
# secret_key 必须从环境变量注入,禁止硬编码兜底——否则 session 可被伪造。
_secret_key = os.environ.get("SECRET_KEY")
if not _secret_key:
raise RuntimeError(
"缺少必需的环境变量 SECRET_KEY请在 .env 中配置(建议: "
"python3 -c 'import secrets;print(secrets.token_hex(32))'"
)
app.secret_key = _secret_key
app.config["TEMPLATES_AUTO_RELOAD"] = True
app.register_blueprint(bp)