diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 31a458bff..506a01d97 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -1,6 +1,6 @@ # 本地开发验证与生产运维 -更新时间:`2026-08-03` +更新时间:`2026-08-05` ## 标准开发流程 @@ -899,6 +899,6 @@ node scripts/rebind-orphan-work-owners.mjs --in --out - `--dry-run`:只统计回填行数,不写文件。 - `--placeholder-user-id`:需要时可覆盖默认占位账号 ID。 -## 维护页目标文件安全边界(2026-08-04) +## 维护页目标文件安全边界(2026-08-05) -`scripts/deploy/maintenance-on.sh` 只允许把同目录临时普通文件原子替换到普通文件或尚不存在的 `page.html` / `enabled` 目标。目标只要是符号链接(包括指向目录的链接)或目录,脚本必须在替换前失败,不能跟随链接把临时文件移入链接目标,也不能打印“已进入维护模式”。跨平台实现继续使用 POSIX `mv -f`,安全语义由替换函数的目标类型门禁保证;修改后运行 `bash -n scripts/deploy/maintenance-on.sh` 与 `npm run check:maintenance-page`。 +`scripts/deploy/maintenance-on.sh` 只允许把同目录临时普通文件原子替换到普通文件或尚不存在的 `page.html` / `enabled` 目标。目标只要是符号链接(包括指向目录的链接)或目录,脚本必须在替换前失败,不能跟随链接把临时文件移入链接目标,也不能打印“已进入维护模式”。`page_temp` 与 `marker_temp` 必须在 `set -u` 下安全初始化,清理 trap 必须在首次 `mktemp` 前生效;任一失败退出都不得在目标同级遗留 `page.html.tmp.*` 或 `enabled.tmp.*`,成功替换后则清空临时路径并解除 trap,不能误删已安装目标。跨平台实现继续使用 POSIX `mv -f`,安全语义由替换函数的目标类型门禁保证;修改后运行 `bash -n scripts/deploy/maintenance-on.sh` 与 `npm run check:maintenance-page`。 diff --git a/scripts/check-maintenance-page.mjs b/scripts/check-maintenance-page.mjs index 9b64607a5..f0ba8e54f 100644 --- a/scripts/check-maintenance-page.mjs +++ b/scripts/check-maintenance-page.mjs @@ -167,6 +167,13 @@ function validateRuntimePageLifecycle() { if (readdirSync(linkedPageTarget).length > 0) { fail('拒绝公告页符号链接后不得把临时文件移入链接目标目录。'); } + if ( + readdirSync(path.dirname(runtimePageFile)).some((entry) => + entry.startsWith(`${path.basename(runtimePageFile)}.tmp.`), + ) + ) { + fail('公告页符号链接校验失败后不得残留 page.html.tmp.* 临时文件。'); + } if (existsSync(markerFile)) { fail('公告页符号链接校验失败时不得创建维护 marker。'); } @@ -193,6 +200,13 @@ function validateRuntimePageLifecycle() { if (readdirSync(linkedMarkerTarget).length > 0) { fail('拒绝 marker 符号链接后不得把临时文件移入链接目标目录。'); } + if ( + readdirSync(path.dirname(markerFile)).some((entry) => + entry.startsWith(`${path.basename(markerFile)}.tmp.`), + ) + ) { + fail('marker 符号链接校验失败后不得残留 enabled.tmp.* 临时文件。'); + } if ( linkedMarkerEnable.stdout.includes('已进入维护模式') || linkedMarkerEnable.stderr.includes('已进入维护模式') diff --git a/scripts/deploy/maintenance-on.sh b/scripts/deploy/maintenance-on.sh index 7b26bea28..c1102c2aa 100644 --- a/scripts/deploy/maintenance-on.sh +++ b/scripts/deploy/maintenance-on.sh @@ -6,6 +6,19 @@ MAINTENANCE_FILE="${GENARRATIVE_MAINTENANCE_FILE:-/var/lib/genarrative/maintenan MAINTENANCE_PAGE_FILE="${GENARRATIVE_MAINTENANCE_PAGE_FILE:-/var/lib/genarrative/maintenance/page.html}" PAGE_SOURCE="" REASON_PARTS=() +page_temp="" +marker_temp="" + +cleanup_temps() { + if [[ -n "${page_temp}" ]]; then + rm -f -- "${page_temp}" + fi + if [[ -n "${marker_temp}" ]]; then + rm -f -- "${marker_temp}" + fi +} + +trap cleanup_temps EXIT replace_file_atomically() { local source_file="$1" @@ -56,7 +69,6 @@ mkdir -p "$(dirname "${MAINTENANCE_FILE}")" "$(dirname "${MAINTENANCE_PAGE_FILE} if [[ -n "${PAGE_SOURCE}" ]]; then page_temp="$(mktemp "${MAINTENANCE_PAGE_FILE}.tmp.XXXXXX")" - trap 'rm -f "${page_temp:-}" "${marker_temp:-}"' EXIT install -m 0644 -- "${PAGE_SOURCE}" "${page_temp}" replace_file_atomically "${page_temp}" "${MAINTENANCE_PAGE_FILE}" page_temp=""