Files
competition-website/scripts/diagnose_upload_download.cjs
qubiaobiao a019df510d 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 变异全检出
2026-09-23 16:34:08 +08:00

196 lines
7.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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('==================================================');
})();