Files
gongxue-base/ocr-reports/misc-scan-comments.jsonl
wangziqi 1289aea7ce docs(ocr): 全库 OCR 审查与修复记录
由 OCR(open-codereview.ai,deepseek-v4-flash)审查产出:
- 审查汇总.md / high-severity-comments.md / all-comments.jsonl / 修复记录.md 等

Reviewed-by: OCR (open-codereview.ai)
2026-08-09 21:29:54 +08:00

24 lines
16 KiB
JSON
Raw 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.

{"path":"ecosystem.config.cjs","start_line":25,"end_line":26,"category":"bug","severity":"medium","content":"Log paths are relative, and PM2 resolves relative `out_file`/`error_file` paths against the working directory where the `pm2` command is launched — not against the app's `cwd` (DEPLOY_DIR). Since this config explicitly supports being started from any directory via DEPLOY_DIR, logs may be written to unexpected locations (or the write fails if the directory doesn't exist there), making them hard for monitoring/rotation to find. Use absolute paths built from DEPLOY_DIR, e.g. `path.join(DEPLOY_DIR, 'logs/backend-error.log')` (require('path') at the top).","suggestion_code":null,"existing_code":" error_file: 'logs/backend-error.log',\n out_file: 'logs/backend-out.log',"}
{"path":"ecosystem.config.cjs","start_line":28,"end_line":29,"category":"bug","severity":"low","content":"No `kill_timeout` is configured. On `pm2 startOrReload` (and restarts), PM2 sends SIGINT and then SIGKILL after a short default grace period (~1.6s), which can abort in-flight HTTP requests and cut DB connections before the Nest app finishes graceful shutdown. Consider adding `kill_timeout: 5000` (and optionally `listen_timeout`) and ensure the app handles SIGINT to drain connections cleanly during deploys.","suggestion_code":null,"existing_code":" autorestart: true,\n watch: false,"}
{"path":"serve-proxy.js","start_line":40,"end_line":46,"category":"bug","severity":"high","content":"`API_TARGET` is read from the environment but never actually used — the proxy hardcodes `127.0.0.1:3000` (hostname, port, and host header are all hardcoded in three places). If `API_TARGET` points to a different host/port, requests are still forwarded to 127.0.0.1:3000, silently breaking the configuration. Parse the target once with `new URL(API_TARGET)` and derive hostname/port/host from it (note `http.request` also can't handle an `https://` target).","suggestion_code":" const target = new URL(API_TARGET);\n const opts = {\n hostname: target.hostname,\n port: target.port || (target.protocol === 'https:' ? 443 : 80),\n path: (target.pathname === '/' ? '' : target.pathname) + req.url,\n method: req.method,\n headers: { ...req.headers, host: target.host },\n };","existing_code":" const opts = {\n hostname: '127.0.0.1',\n port: 3000,\n path: req.url,\n method: req.method,\n headers: { ...req.headers, host: '127.0.0.1:3000' },\n };"}
{"path":"serve-proxy.js","start_line":29,"end_line":34,"category":"bug","severity":"medium","content":"The catch-all fallback swallows every read error (ENOENT, EACCES, etc.) and returns `index.html` with a 200 for ANY missing file — including missing `.js`/`.css` assets. This serves HTML where a script module was expected (breaking module loading and masking real deployment problems) and never produces a 404. Worse, if `apps/admin/dist/index.html` itself is missing, `fs.readFileSync` throws inside the catch block, propagates out of the request handler, and crashes the process. Only fall back for navigation requests (no file extension / `Accept: text/html`) and return a proper 404 for missing assets; guard the index.html read as well.","suggestion_code":null,"existing_code":" } catch {\n // SPA fallback: return index.html\n const index = fs.readFileSync(path.join(STATIC_DIR, 'index.html'));\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(index);\n }"}
{"path":"serve-proxy.js","start_line":26,"end_line":27,"category":"performance","severity":"medium","content":"`fs.readFileSync` blocks the Node event loop on every request. Under load, all concurrent requests are serialized behind synchronous file I/O, and large files are fully buffered in memory. Use async `fs.promises.readFile` or stream the file with `fs.createReadStream`.","suggestion_code":" const stream = fs.createReadStream(filePath);\n res.writeHead(200, { 'Content-Type': mime, 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=604800' });\n stream.pipe(res);","existing_code":" const content = fs.readFileSync(filePath);\n res.writeHead(200, { 'Content-Type': mime, 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=604800' });"}
{"path":"serve-proxy.js","start_line":47,"end_line":50,"category":"bug","severity":"medium","content":"Two proxy robustness issues: (1) `proxyRes.headers` is forwarded verbatim, including hop-by-hop headers (`connection`, `keep-alive`, `transfer-encoding`, `upgrade`, …). Because the response is piped, Node sets its own `transfer-encoding`/`content-length`, which can conflict with the forwarded headers and cause protocol errors or truncated responses — strip hop-by-hop headers before `writeHead`. (2) `proxy.on('error')` can fire after headers have already been written (mid-stream upstream failure or client disconnect), so the `res.writeHead(502)` call will throw uncaught; guard with `res.headersSent`, add a timeout for the upstream connection, and handle errors from `req.pipe(proxy)` / `proxyRes.pipe(res)`.","suggestion_code":null,"existing_code":" const proxy = http.request(opts, (proxyRes) => {\n res.writeHead(proxyRes.statusCode, proxyRes.headers);\n proxyRes.pipe(res);\n });"}
{"path":"serve-proxy.js","start_line":60,"end_line":62,"category":"security","severity":"low","content":"Because every request URL is absolute (starts with `/`), `path.normalize` already clips leading `..` at the root, so the `replace(/^((\\.\\.(\\/|\\\\|$))+)/, '')` regex never matches anything — it's misleading dead code. Percent-encoded traversal (`%2e%2e%2f`) is neither decoded nor checked, and `path.join`/`normalize` semantics are platform-dependent. Decode the URL with `decodeURIComponent` and explicitly verify the resolved path stays inside `STATIC_DIR` (e.g., `path.resolve(...)` and `startsWith(STATIC_DIR + path.sep)`), returning 403/404 otherwise; this also fixes serving filenames containing spaces/encoded characters.","suggestion_code":null,"existing_code":" const urlPath = req.url === '/' ? '/index.html' : req.url.split('?')[0];\n const safePath = path.normalize(urlPath).replace(/^(\\.\\.(\\/|\\\\|$))+/, '');\n serveStatic(res, path.join(STATIC_DIR, safePath));"}
{"path":"migrate.sh","start_line":19,"end_line":19,"category":"security","severity":"high","content":"密码以 `-p\"${MYSQL_PASS}\"` 形式拼在命令行参数中,会出现在 `ps aux` 进程列表里,同机其他用户可直接看到明文密码。建议改用 `docker compose exec -T -e MYSQL_PWD=\"${MYSQL_PASS}\" mysql mysql -u root ...` 方式传入,避免密码暴露在进程列表中。","suggestion_code":"gunzip -c \"$DUMP\" | docker compose exec -T -e MYSQL_PWD=\"${MYSQL_PASS}\" mysql mysql -u root dorm_billing","existing_code":"gunzip -c \"$DUMP\" | docker compose exec -T mysql mysql -u root -p\"${MYSQL_PASS}\" dorm_billing"}
{"path":"migrate.sh","start_line":16,"end_line":16,"category":"security","severity":"medium","content":"硬编码默认密码 `gongxue_2024` 作为 `.env` 缺失时的回退值。该凭据一旦随代码库泄露即等于泄露,且会静默使用弱口令/已知口令部署。建议强制要求 MYSQL_ROOT_PASSWORD 从 `.env` 或环境变量注入,缺失时直接报错退出,而不是回退到默认值。","suggestion_code":null,"existing_code":"MYSQL_PASS=\"${MYSQL_ROOT_PASSWORD:-gongxue_2024}\""}
{"path":"migrate.sh","start_line":37,"end_line":37,"category":"security","severity":"high","content":"脚本最后把含明文密码的完整命令 `-p${MYSQL_PASS}` echo 到终端/日志,直接泄露凭据(也会进入 CI/终端历史)。建议只输出不含密码的验证命令,例如提示使用 MYSQL_PWD 或提示输入密码。","suggestion_code":"echo \" docker compose exec -e MYSQL_PWD=<password> mysql mysql -u root gongxue -e 'SELECT COUNT(*) FROM students'\"","existing_code":"echo \" docker compose exec mysql mysql -u root -p${MYSQL_PASS} gongxue -e 'SELECT COUNT(*) FROM students'\""}
{"path":"migrate.sh","start_line":25,"end_line":25,"category":"bug","severity":"medium","content":"固定 `sleep 8` 无法保证 TypeORM synchronize 已完成建表:机器较慢或应用启动耗时较长时,脚本会提前进入步骤 3导致 migrate-legacy.sql 因目标表不存在而失败(且失败后处于不一致状态)。建议改为轮询等待就绪条件(如循环检查目标表 `gongxue.students` 是否存在,或等待应用健康检查通过)再继续。","suggestion_code":null,"existing_code":"sleep 8"}
{"path":"migrate.sh","start_line":29,"end_line":29,"category":"bug","severity":"medium","content":"`migrate-legacy.sql` 通过 stdin 直接执行,未包裹在事务中;若中途失败,数据将部分迁移、库处于不一致状态,且脚本没有备份/回滚机制。建议在 SQL 内显式使用 `START TRANSACTION ... COMMIT`(确认目标表为 InnoDB或迁移前先备份目标库。","suggestion_code":null,"existing_code":"docker compose exec -T mysql mysql -u root -p\"${MYSQL_PASS}\" < migrate-legacy.sql"}
{"path":"migrate.sh","start_line":24,"end_line":24,"category":"bug","severity":"medium","content":"脚本没有 trap 清理逻辑:若在 `pm2 start` 之后、`pm2 stop` 之前任一步失败,`set -e` 直接退出会留下运行中的应用;反之在 `pm2 stop` 后失败,应用会一直处于停止状态。另外,若 backend 已在运行(非严格首次运行场景),`pm2 start` 会因重复启动失败直接终止脚本。建议用 trap 在失败时恢复应用状态,并在 start 前先检测/处理已运行实例。","suggestion_code":null,"existing_code":"DB_SYNCHRONIZE=true pm2 start ecosystem.config.cjs --only gongxue-backend"}
{"path":"migrate.sh","start_line":29,"end_line":29,"category":"maintainability","severity":"low","content":"`migrate-legacy.sql` 使用相对路径,脚本从其他工作目录执行时会找不到文件。建议在脚本开头 `cd \"$(dirname \"$0\")\"` 或使用相对脚本目录的绝对路径,保证脚本在任意目录下可运行。","suggestion_code":null,"existing_code":"docker compose exec -T mysql mysql -u root -p\"${MYSQL_PASS}\" < migrate-legacy.sql"}
{"path":"deploy.sh","start_line":34,"end_line":39,"category":"bug","severity":"high","content":"Remote commands run without `set -e` (only the local shell enables `set -euo pipefail`). The ssh exit code is determined solely by the last remote command (`pm2 status`), so if `npm ci` or `npm run migration:run -w @gongxue/server` fails, the script still reloads PM2 and prints \"部署完成!\" — a failed migration is silently swallowed and the deploy is reported as successful. Add `set -euo pipefail` at the start of the remote command string (right after `cd ${REMOTE_DIR}`) or check each step's exit code explicitly.","suggestion_code":null,"existing_code":" echo '执行数据库迁移...'\n npm run migration:run -w @gongxue/server\n echo 'PM2 重载...'\n pm2 startOrReload ecosystem.config.cjs --update-env\n pm2 save\n pm2 status"}
{"path":"deploy.sh","start_line":17,"end_line":24,"category":"security","severity":"high","content":"`--delete` will remove any server-only files that are not excluded. The repo contains no committed `.env` (only `.env.example`), so the production `.env` created manually on the server would be deleted on every deploy, and a local dev `.env` (if present) would overwrite the production one — leaking/erasing secrets. Add `--exclude='.env'` / `--exclude='.env.*'` and exclude any other server-only runtime data (e.g. `uploads/`) that must survive deploys.","suggestion_code":null,"existing_code":"rsync -avz --delete \\\n --exclude='node_modules' \\\n --exclude='.git' \\\n --exclude='*.db' \\\n --exclude='.DS_Store' \\\n --exclude='logs/' \\\n --exclude='.turbo/' \\\n ./ \"${SSH_HOST}:${REMOTE_DIR}/\""}
{"path":"deploy.sh","start_line":30,"end_line":33,"category":"bug","severity":"high","content":"`npm ci` runs only when `node_modules` does not exist on the server. On subsequent deploys, changes to `package.json`/`package-lock.json` (new or removed dependencies) are never installed because `node_modules` is excluded from rsync and already present remotely — production can run with stale dependencies while the local build (used to produce the deployed bundle) was built with new ones. Always run `npm ci --omit=dev` on the server, or trigger it by comparing the remote and local `package-lock.json` checksums.","suggestion_code":null,"existing_code":" if [ ! -d node_modules ]; then\n echo '首次部署,安装依赖...'\n npm ci --omit=dev\n fi"}
{"path":"deploy.sh","start_line":44,"end_line":44,"category":"bug","severity":"low","content":"The fallback `|| curl -s ifconfig.me` is ineffective: `hostname -I 2>/dev/null | awk \"{print $1}\"` is a pipeline whose exit status is that of `awk`, which still exits 0 (with empty output) when `hostname -I` fails. The script then prints `http://` with no host. Capture the IP into a variable, validate it is non-empty, and only then fall back to curl.","suggestion_code":null,"existing_code":"echo \"访问: http://$(ssh \"${SSH_HOST}\" 'hostname -I 2>/dev/null | awk \"{print \\$1}\" || curl -s ifconfig.me')\""}
{"path":"oxlint.config.ts","start_line":10,"end_line":12,"category":"maintainability","severity":"low","content":"The React version is hardcoded to '19.0.0' with no match found in any package.json in this repo. This setting can silently drift from the actual dependency version, causing react-plugin rules to evaluate against a stale version. Prefer `version: 'detect'` (auto-detect from package.json) or keep this value in sync with the real dependency.","suggestion_code":null,"existing_code":" react: {\n version: '19.0.0',\n },"}
{"path":"oxlint.config.ts","start_line":6,"end_line":6,"category":"maintainability","severity":"low","content":"Globally disabling `typescript/no-explicit-any` removes the type-safety guardrail for the entire codebase, contradicting the project's TypeScript quality requirement of avoiding `any`. If `any` is only needed in a few places, keep the rule enabled and use targeted `// eslint-disable` comments (or annotate the `any` usage with a justification) instead of turning it off globally.","suggestion_code":null,"existing_code":" 'typescript/no-explicit-any': 'off',"}
{"path":".gitea/workflows/deploy.yml","start_line":53,"end_line":56,"category":"security","severity":"high","content":"`--delete` will remove any files on the server that are not present in the source tree. Runtime config such as `.env` / `.env.*` (typically gitignored and thus absent from the repo) will be silently deleted on every deploy, breaking DB credentials and other environment settings. Add `--exclude='.env'` (and `--exclude='.env.*'`) or keep server-side config outside REMOTE_DIR.","suggestion_code":null,"existing_code":" rsync -avz --delete \\\n --exclude='node_modules' \\\n --exclude='.git' \\\n --exclude='*.db' \\"}
{"path":".gitea/workflows/deploy.yml","start_line":69,"end_line":72,"category":"bug","severity":"high","content":"Dependencies are only installed when `node_modules` does not exist. On subsequent deploys, changes to `package.json` / `package-lock.json` will never be installed, so the server keeps running stale dependency versions after the code has been updated. Run `npm ci --omit=dev` unconditionally (or compare a lockfile hash / mtime) instead of gating on directory existence.","suggestion_code":null,"existing_code":" if [ ! -d node_modules ]; then\n echo '首次部署,安装生产依赖...'\n npm ci --omit=dev\n fi"}
{"path":".gitea/workflows/deploy.yml","start_line":16,"end_line":17,"category":"bug","severity":"medium","content":"No `concurrency` control is defined for this workflow. Two manual dispatches run at the same time can race on rsync `--delete`, run database migrations concurrently, and interfere with PM2 reload, potentially corrupting the deployed directory. Add a `concurrency` group (e.g. with `cancel-in-progress: true`) keyed to the deploy target.","suggestion_code":null,"existing_code":"on:\n workflow_dispatch:"}