背景(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 变异全检出
196 lines
7.7 KiB
JavaScript
196 lines
7.7 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* 生产环境 上传/下载 链路诊断脚本
|
||
* ---------------------------------------------------------------------------
|
||
* 用途:在生产后端目录下运行,回答三个问题:
|
||
* 1) app/public/{works,answers,materials,tmp} 目录是否存在、是否可写?
|
||
* 2) 代码里「上传存盘路径」与「下载读取路径」是否真的指向同一文件?(路径自洽性)
|
||
* 3) 数据库里已有的 submission 记录,其 file_path 是否对应磁盘上真实存在的文件?
|
||
*
|
||
* 运行:
|
||
* cd <生产 backend 目录>
|
||
* node scripts/diagnose_upload_download.cjs
|
||
* # 如需抽查数据库(可选):
|
||
* DB_HOST=127.0.0.1 DB_USER=root DB_PASS='<本机 DB 口令>' DB_NAME=competition_db \
|
||
* node scripts/diagnose_upload_download.cjs
|
||
*
|
||
* 不依赖 Egg 启动,纯 Node + fs 即可;DB 部分为可选(拿不到模块/连接失败则跳过)。
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// ---------- 1. 定位 backend 根目录(app/public 的上一级) ----------
|
||
function resolveBackendRoot() {
|
||
// 脚本放在 backend/scripts/ 下时,根目录是 scripts 的上一级
|
||
let here = __dirname;
|
||
for (let i = 0; i < 4; i++) {
|
||
if (fs.existsSync(path.join(here, 'app', 'public'))) return here;
|
||
const parent = path.dirname(here);
|
||
if (parent === here) break;
|
||
here = parent;
|
||
}
|
||
// 兜底:以当前工作目录为根
|
||
return process.cwd();
|
||
}
|
||
|
||
const root = resolveBackendRoot();
|
||
const publicDir = path.join(root, 'app', 'public');
|
||
|
||
console.log('==================================================');
|
||
console.log(' 上传/下载 链路诊断');
|
||
console.log('==================================================');
|
||
console.log('backend 根目录 :', root);
|
||
console.log('app/public :', publicDir);
|
||
console.log('app/public 存在:', fs.existsSync(publicDir));
|
||
console.log('');
|
||
|
||
// ---------- 2. 检查各业务目录的存在性与可写性 ----------
|
||
const subDirs = ['works', 'answers', 'materials', 'tmp'];
|
||
const groups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi'];
|
||
|
||
function checkDir(p) {
|
||
const exists = fs.existsSync(p);
|
||
let type = exists ? (fs.statSync(p).isDirectory() ? '目录' : '非目录!') : '不存在';
|
||
let writable = false;
|
||
if (exists) {
|
||
try {
|
||
const probe = path.join(p, '.write_probe_' + Date.now());
|
||
fs.writeFileSync(probe, 'x');
|
||
fs.unlinkSync(probe);
|
||
writable = true;
|
||
} catch (e) {
|
||
writable = false;
|
||
}
|
||
}
|
||
return { exists, type, writable };
|
||
}
|
||
|
||
console.log('--- 2.1 业务目录存在性 / 可写性 ---');
|
||
const dirReport = {};
|
||
for (const d of subDirs) {
|
||
const full = path.join(publicDir, d);
|
||
const r = checkDir(full);
|
||
dirReport[d] = r;
|
||
console.log(
|
||
` app/public/${d.padEnd(9)} : ${r.type.padEnd(8)} 可写=${r.writable}`
|
||
);
|
||
if (d === 'works' || d === 'materials') {
|
||
for (const g of groups) {
|
||
const gp = path.join(full, g);
|
||
const gr = checkDir(gp);
|
||
console.log(
|
||
` └─ ${g.padEnd(9)} : ${gr.type.padEnd(8)} 可写=${gr.writable}`
|
||
);
|
||
}
|
||
}
|
||
}
|
||
console.log('');
|
||
|
||
// ---------- 3. 路径自洽性模拟(与源码逻辑一致) ----------
|
||
console.log('--- 3. 路径自洽性(模拟一次上传→下载应指向同一文件)---');
|
||
function simulate(group, safeName) {
|
||
// 上传存盘:service.upload.saveWorkFile / admin.uploadMaterial / exam.uploadWord
|
||
const savedAbs = path.join(publicDir, 'works', group, safeName); // 例:works/benke/...
|
||
// 下载读取:admin.downloadSubmissionFile -> path.join(app.baseDir,'app/public',submission.file_path)
|
||
const dbRelative = path.join('works', group, safeName).replace(/\\/g, '/');
|
||
const readAbs = path.join(publicDir, dbRelative);
|
||
const ok = path.resolve(savedAbs) === path.resolve(readAbs);
|
||
console.log(` ${group}/${safeName}`);
|
||
console.log(` 上传落盘路径 : ${savedAbs}`);
|
||
console.log(` 下载读取路径 : ${readAbs}`);
|
||
console.log(` 自洽一致 : ${ok ? '✓ YES' : '✗ NO —— 代码有路径错配!'}`);
|
||
return ok;
|
||
}
|
||
const selfConsistent =
|
||
simulate('benke', 'work_1700000000000_ab12cd.docx') &&
|
||
simulate('zhongzhi', 'exam_1_1700000000000.docx') &&
|
||
simulate('zhongzhi', '报告.pdf');
|
||
console.log('');
|
||
|
||
// ---------- 4. 抽查数据库里 submission 的 file_path 是否真实存在 ----------
|
||
console.log('--- 4. 数据库 submission.file_path 真实性抽查(可选)---');
|
||
const dbCfg = {
|
||
host: process.env.DB_HOST || '127.0.0.1',
|
||
user: process.env.DB_USER || 'root',
|
||
password: process.env.DB_PASS,
|
||
database: process.env.DB_NAME || 'competition_db',
|
||
};
|
||
|
||
(async () => {
|
||
let mysqlOk = false;
|
||
try {
|
||
const mysql = require(path.join(root, 'node_modules', 'mysql2', 'promise'));
|
||
const conn = await mysql.createConnection({
|
||
host: dbCfg.host,
|
||
user: dbCfg.user,
|
||
password: dbCfg.password,
|
||
database: dbCfg.database,
|
||
connectTimeout: 4000,
|
||
});
|
||
mysqlOk = true;
|
||
console.log(` 已连接数据库 ${dbCfg.database} @ ${dbCfg.host}`);
|
||
|
||
const tables = [
|
||
{ name: 'benke_submissions', label: '本科', col: 'file_path' },
|
||
{ name: 'jiaoshi_submissions', label: '教师', col: 'file_path' },
|
||
{ name: 'exam_submissions', label: '中职/高职(word)', col: 'word_file_path' },
|
||
];
|
||
for (const t of tables) {
|
||
try {
|
||
const [rows] = await conn.query(
|
||
`SELECT id, ${t.col} FROM ${t.name} WHERE ${t.col} IS NOT NULL AND ${t.col} <> '' LIMIT 10`
|
||
);
|
||
console.log(` [${t.label}] ${t.name}.${t.col} 抽样 ${rows.length} 条:`);
|
||
if (rows.length === 0) {
|
||
console.log(' (无带路径的记录,或该表无文件)');
|
||
}
|
||
for (const r of rows) {
|
||
const fp = r[t.col];
|
||
const abs = path.join(publicDir, fp);
|
||
const exists = fs.existsSync(abs);
|
||
console.log(
|
||
` id=${r.id} file_path="${fp}" -> 磁盘${exists ? '✓ 存在' : '✗ 缺失!'}`
|
||
);
|
||
if (!exists) console.log(` 解析绝对路径: ${abs}`);
|
||
}
|
||
} catch (e) {
|
||
console.log(` [${t.label}] 查询 ${t.name} 失败: ${e.message}`);
|
||
}
|
||
}
|
||
await conn.end();
|
||
} catch (e) {
|
||
console.log(` 数据库检查跳过(未连接: ${e.message})`);
|
||
console.log(' 如需抽查,请在环境变量中提供 DB_HOST/DB_USER/DB_PASS/DB_NAME 后重跑。');
|
||
}
|
||
console.log('');
|
||
|
||
// ---------- 5. 结论 ----------
|
||
console.log('==================================================');
|
||
console.log(' 诊断结论');
|
||
console.log('==================================================');
|
||
const dirsMissing = subDirs.filter(d => !dirReport[d].exists);
|
||
const dirsNotWritable = subDirs.filter(d => dirReport[d].exists && !dirReport[d].writable);
|
||
if (dirsMissing.length) {
|
||
console.log(`✗ 关键目录缺失: app/public/${dirsMissing.join(', ')}`);
|
||
console.log(' → 上传文件无处落盘,下载必然 404。请在服务器后端目录执行:');
|
||
for (const d of dirsMissing) {
|
||
console.log(` mkdir -p app/public/${d}/{zhongzhi,gaozhi,benke,jiaoshi}`);
|
||
}
|
||
} else {
|
||
console.log('✓ 业务目录均存在');
|
||
}
|
||
if (dirsNotWritable.length) {
|
||
console.log(`✗ 目录不可写: app/public/${dirsNotWritable.join(', ')}`);
|
||
console.log(' → 检查进程用户对该目录的写权限(chown/chmod)。');
|
||
} else {
|
||
console.log('✓ 业务目录均可写');
|
||
}
|
||
console.log(selfConsistent ? '✓ 代码路径自洽(上传/下载指向同一文件)' : '✗ 代码路径不自洽,需修代码');
|
||
if (mysqlOk) {
|
||
console.log(' → 查看上方「磁盘缺失」标记:若有,说明文件没落盘(上传未成功或目录曾缺失)。');
|
||
}
|
||
console.log('==================================================');
|
||
})();
|