chore(security): 处置已入库的本地 DB 口令(P1-P3;P4 改历史不做)
背景(R14 登记的 §16.3):
git grep -l "74191221" HEAD 曾命中 7 个文件,全为本地 MariaDB root 口令
(已核实无生产凭据)。本次按登记项执行 P1-P3;P4(git filter-repo 改写
历史)明确不做。
P1 database/config.json(dev/test 两段明文口令)
- 加入 .gitignore
- 新增 database/config.json.example(口令为 CHANGE_ME 占位符)
- git rm --cached(保留磁盘文件,本地 sequelize-cli 不受影响)
- 注意:这是对共享仓库的文件移除。push 后其他克隆需
cp database/config.json.example database/config.json 并填入口令。
P2 五个脚本口令环境变量化(去明文兜底)
- add_word_cols.js / add_wx_link.js / show_tables.js / seed-carousel.js
内联 '74191221' 改为 process.env.DB_PASSWORD(另支持 DB_HOST/DB_USER/DB_NAME)
- scripts/diagnose_upload_download.cjs:process.env.DB_PASS || '74191221'
改为 process.env.DB_PASS;用法注释中的示例口令改为占位符
- 有意的行为变更:未注入 DB_PASSWORD 时明确失败,不再静默使用公开口令
P3 config/config.default.js 消除「生产静默回退公开口令」
- sequelize.password 去掉 || '74191221'
- redis.client.password 去掉 || '74191221'
- 生产安全守卫新增 DB_PASSWORD 缺失阻断(与 JWT_SECRET/APP_KEYS 同源同形)
- deploy/.env.production 本就含 DB_PASSWORD ⇒ 现网零影响
验证(全部实测):
- git grep 74191221 工作树零命中(HEAD 仍有,属 P4 不做范围)
- 裸启动(无任何 DB 变量)成功:本地口令由 config.local.js 提供
- 端到端:health db:ok + admin-login code=0
- MySQL 侧独立取证:processlist = root @ competition_db
- 变异测试(改坏 config.local.js 口令)⇒ 报 1045 且用户名仍为 root
⇒ 同证 P3 兜底确已移除 + R18 的生产账号过滤仍生效
- P2 双向验证:无 DB_PASSWORD 失败 / 注入后正常列 27 张表
- 门禁四连:verify_scoring_e2e 17/0、verify_exam_submit_integrity 46/0、
verify_big_question --cleanup(清理+窗口还原)、check_no_proxy_guard 5 变异全检出
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -27,6 +27,11 @@ run/
|
|||||||
# 含本地 DB 口令,故排除入库;如需团队共享请提交不含口令的 config.local.example.js
|
# 含本地 DB 口令,故排除入库;如需团队共享请提交不含口令的 config.local.example.js
|
||||||
config/config.local.js
|
config/config.local.js
|
||||||
|
|
||||||
|
# sequelize-cli 数据库配置(含本地 DB 口令)—— 2026-09-23 入库口令处置 P1
|
||||||
|
# 仓库内保留不含口令的 database/config.json.example;使用前复制并填入口令:
|
||||||
|
# cp database/config.json.example database/config.json
|
||||||
|
database/config.json
|
||||||
|
|
||||||
# Coverage / cache
|
# Coverage / cache
|
||||||
coverage/
|
coverage/
|
||||||
.cache/
|
.cache/
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
const mysql = require('mysql2/promise');
|
const mysql = require('mysql2/promise');
|
||||||
(async () => {
|
(async () => {
|
||||||
|
// 🔴 口令不再内置明文(2026-09-23 入库口令处置 P2)。运行前请注入 DB_PASSWORD:
|
||||||
|
// DB_PASSWORD='<本机 DB 口令>' node add_word_cols.js
|
||||||
const c = await mysql.createConnection({
|
const c = await mysql.createConnection({
|
||||||
host: 'localhost', user: 'root', password: '74191221', database: 'competition_db'
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
user: process.env.DB_USER || 'root',
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME || 'competition_db',
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await c.query("ALTER TABLE exam_submissions ADD COLUMN word_file_path VARCHAR(500) NULL DEFAULT NULL COMMENT 'Word答案文件路径' AFTER answers");
|
await c.query("ALTER TABLE exam_submissions ADD COLUMN word_file_path VARCHAR(500) NULL DEFAULT NULL COMMENT 'Word答案文件路径' AFTER answers");
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
const mysql = require('mysql2/promise');
|
const mysql = require('mysql2/promise');
|
||||||
(async () => {
|
(async () => {
|
||||||
|
// 🔴 口令不再内置明文(2026-09-23 入库口令处置 P2)。运行前请注入 DB_PASSWORD:
|
||||||
|
// DB_PASSWORD='<本机 DB 口令>' node add_wx_link.js
|
||||||
const c = await mysql.createConnection({
|
const c = await mysql.createConnection({
|
||||||
host: 'localhost', user: 'root', password: '74191221', database: 'competition_db'
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
user: process.env.DB_USER || 'root',
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME || 'competition_db',
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await c.query("ALTER TABLE news ADD COLUMN wx_link VARCHAR(500) NULL DEFAULT NULL COMMENT '微信公众号文章链接' AFTER content");
|
await c.query("ALTER TABLE news ADD COLUMN wx_link VARCHAR(500) NULL DEFAULT NULL COMMENT '微信公众号文章链接' AFTER content");
|
||||||
|
|||||||
@@ -217,8 +217,10 @@ module.exports = (appInfo) => {
|
|||||||
port: parseInt(process.env.DB_PORT, 10) || 3306,
|
port: parseInt(process.env.DB_PORT, 10) || 3306,
|
||||||
// 兼容 docker-compose/.env.example 中的 DB_USER 与代码历史使用的 DB_USERNAME
|
// 兼容 docker-compose/.env.example 中的 DB_USER 与代码历史使用的 DB_USERNAME
|
||||||
username: process.env.DB_USERNAME || process.env.DB_USER || 'root',
|
username: process.env.DB_USERNAME || process.env.DB_USER || 'root',
|
||||||
// 【测试模拟值 '74191221',仅供本地/测试部署;生产请通过 DB_PASSWORD 环境变量注入真实密码】
|
// 🔴 口令不再内置明文兜底(2026-09-23 入库口令处置 P3):
|
||||||
password: process.env.DB_PASSWORD || '74191221',
|
// 生产 —— 必须由 .env.production 注入 DB_PASSWORD(缺失则由下方生产安全守卫阻断启动);
|
||||||
|
// 本地 —— 由 config/config.local.js(已 gitignore)深合并覆盖为真实本机口令。
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
timezone: '+08:00',
|
timezone: '+08:00',
|
||||||
define: {
|
define: {
|
||||||
underscored: true,
|
underscored: true,
|
||||||
@@ -279,7 +281,8 @@ module.exports = (appInfo) => {
|
|||||||
client: {
|
client: {
|
||||||
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
|
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
|
||||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||||
password: process.env.REDIS_PASSWORD || '74191221',
|
// 🔴 同上:不再内置明文兜底(2026-09-23 P3)。Redis 当前全库无使用点,留空为零影响。
|
||||||
|
password: process.env.REDIS_PASSWORD || '',
|
||||||
db: 0,
|
db: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -442,6 +445,11 @@ module.exports = (appInfo) => {
|
|||||||
const insecure = [];
|
const insecure = [];
|
||||||
if (config.jwt.secret === DEFAULT_JWT_SECRET) insecure.push('JWT_SECRET');
|
if (config.jwt.secret === DEFAULT_JWT_SECRET) insecure.push('JWT_SECRET');
|
||||||
if (config.keys === DEFAULT_APP_KEYS) insecure.push('APP_KEYS');
|
if (config.keys === DEFAULT_APP_KEYS) insecure.push('APP_KEYS');
|
||||||
|
// 🔴 2026-09-23(入库口令处置 P3):口令缺失同样阻断。
|
||||||
|
// 原实现在此处会静默回退到公开的开发口令,等价于「生产用公开口令连库」。
|
||||||
|
// 现要求生产必须显式注入 DB_PASSWORD(deploy/.env.production 本就含该键,
|
||||||
|
// 故现网永不触发本分支;一旦触发说明注入链路断了,明确阻断优于连不上库)。
|
||||||
|
if (!config.sequelize.password) insecure.push('DB_PASSWORD');
|
||||||
if (insecure.length > 0) {
|
if (insecure.length > 0) {
|
||||||
// 生产环境仍在使用硬编码默认密钥属于严重安全隐患,直接阻断启动
|
// 生产环境仍在使用硬编码默认密钥属于严重安全隐患,直接阻断启动
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"development": {
|
"development": {
|
||||||
"username": "root",
|
"username": "root",
|
||||||
"password": "74191221",
|
"password": "CHANGE_ME_local_db_password",
|
||||||
"database": "competition_db",
|
"database": "competition_db",
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 3306,
|
"port": 3306,
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"username": "root",
|
"username": "root",
|
||||||
"password": "74191221",
|
"password": "CHANGE_ME_local_db_password",
|
||||||
"database": "competition_db_test",
|
"database": "competition_db_test",
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 3306,
|
"port": 3306,
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
* cd <生产 backend 目录>
|
* cd <生产 backend 目录>
|
||||||
* node scripts/diagnose_upload_download.cjs
|
* node scripts/diagnose_upload_download.cjs
|
||||||
* # 如需抽查数据库(可选):
|
* # 如需抽查数据库(可选):
|
||||||
* DB_HOST=127.0.0.1 DB_USER=root DB_PASS=74191221 DB_NAME=competition_db \
|
* DB_HOST=127.0.0.1 DB_USER=root DB_PASS='<本机 DB 口令>' DB_NAME=competition_db \
|
||||||
* node scripts/diagnose_upload_download.cjs
|
* node scripts/diagnose_upload_download.cjs
|
||||||
*
|
*
|
||||||
* 不依赖 Egg 启动,纯 Node + fs 即可;DB 部分为可选(拿不到模块/连接失败则跳过)。
|
* 不依赖 Egg 启动,纯 Node + fs 即可;DB 部分为可选(拿不到模块/连接失败则跳过)。
|
||||||
@@ -114,7 +114,7 @@ console.log('--- 4. 数据库 submission.file_path 真实性抽查(可选)--
|
|||||||
const dbCfg = {
|
const dbCfg = {
|
||||||
host: process.env.DB_HOST || '127.0.0.1',
|
host: process.env.DB_HOST || '127.0.0.1',
|
||||||
user: process.env.DB_USER || 'root',
|
user: process.env.DB_USER || 'root',
|
||||||
password: process.env.DB_PASS || '74191221',
|
password: process.env.DB_PASS,
|
||||||
database: process.env.DB_NAME || 'competition_db',
|
database: process.env.DB_NAME || 'competition_db',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
const { Sequelize } = require('./node_modules/sequelize');
|
const { Sequelize } = require('./node_modules/sequelize');
|
||||||
const sequelize = new Sequelize('competition_db', 'root', '74191221', {
|
// 🔴 口令不再内置明文(2026-09-23 入库口令处置 P2)。运行前请注入 DB_PASSWORD:
|
||||||
host: 'localhost', port: 3306, dialect: 'mysql',
|
// DB_PASSWORD='<本机 DB 口令>' node seed-carousel.js
|
||||||
logging: false
|
const sequelize = new Sequelize(
|
||||||
});
|
process.env.DB_NAME || 'competition_db',
|
||||||
|
process.env.DB_USER || 'root',
|
||||||
|
process.env.DB_PASSWORD,
|
||||||
|
{
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT, 10) || 3306,
|
||||||
|
dialect: 'mysql',
|
||||||
|
logging: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
const mysql = require('mysql2/promise');
|
const mysql = require('mysql2/promise');
|
||||||
(async () => {
|
(async () => {
|
||||||
|
// 🔴 口令不再内置明文(2026-09-23 入库口令处置 P2)。运行前请注入 DB_PASSWORD:
|
||||||
|
// DB_PASSWORD='<本机 DB 口令>' node show_tables.js
|
||||||
const c = await mysql.createConnection({
|
const c = await mysql.createConnection({
|
||||||
host: 'localhost', user: 'root', password: '74191221', database: 'competition_db'
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
user: process.env.DB_USER || 'root',
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME || 'competition_db',
|
||||||
});
|
});
|
||||||
const [t] = await c.query('SHOW TABLES');
|
const [t] = await c.query('SHOW TABLES');
|
||||||
t.forEach(r => console.log(Object.values(r)[0]));
|
t.forEach(r => console.log(Object.values(r)[0]));
|
||||||
|
|||||||
Reference in New Issue
Block a user