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)
This commit is contained in:
23
ocr-reports/misc-scan-comments.jsonl
Normal file
23
ocr-reports/misc-scan-comments.jsonl
Normal file
@@ -0,0 +1,23 @@
|
||||
{"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:"}
|
||||
Reference in New Issue
Block a user