preview host 허용 회귀 가드 추가 (check:preview-hosts)
배포 도메인 블랭크(vite preview allowedHosts 누락) 재발을 막는 행동 검증 스크립트를 추가. preview를 띄워 공개 Host(vnet/api-vnet/chanpaca) 3종이 200으로 허용되고 임의 Host는 403으로 차단(DNS 리바인딩 보호 유지)되는지 http.request로 직접 Host 헤더를 보내 확인한다. package.json에 check:preview-hosts로 등록. 검증: npm run check:preview-hosts → 공개 3 allowed + unlisted blocked PASS, exit 0. 대시보드 104차 갱신.
This commit is contained in:
parent
7b2ceafd68
commit
5c4ac04e06
3 changed files with 99 additions and 1 deletions
97
apps/web/scripts/check-preview-hosts.mjs
Normal file
97
apps/web/scripts/check-preview-hosts.mjs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-preview-hosts — vite preview 서버의 host 허용 회귀 가드.
|
||||
*
|
||||
* 배포는 `npm run preview`를 cloudflared 터널로 공개 도메인에 연결한다.
|
||||
* vite 6 preview는 Host 헤더를 `preview.allowedHosts`로 검증하므로, 공개 호스트가
|
||||
* 목록에 없으면 "Blocked request. This host is not allowed." 403만 반환되어
|
||||
* 배포 도메인 전체가 블랭크가 된다(2026-06-30 회귀). 이 스크립트는 dist를 preview로
|
||||
* 띄워, 공개 Host는 통과(200, app HTML)하고 임의 Host는 차단되는지 확인한다.
|
||||
*
|
||||
* Node의 fetch는 Host 헤더 오버라이드를 무시하므로 http.request로 직접 Host를 보낸다.
|
||||
*
|
||||
* 사용: node scripts/check-preview-hosts.mjs (dist 빌드가 선행되어야 한다)
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import http from "node:http";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
|
||||
const PORT = Number(process.env.PREVIEW_CHECK_PORT ?? 5191);
|
||||
const PUBLIC_HOSTS = ["vnet.18ka.net", "api-vnet.18ka.net", "vignette.chanpaca.net"];
|
||||
const DISALLOWED_HOST = "evil.example.com";
|
||||
const BLOCK_MARKER = "Blocked request";
|
||||
|
||||
const isWin = process.platform === "win32";
|
||||
const viteBin = isWin ? "node_modules\\.bin\\vite.cmd" : "node_modules/.bin/vite";
|
||||
|
||||
function startPreview() {
|
||||
return spawn(viteBin, ["preview", "--host", "127.0.0.1", "--port", String(PORT)], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
shell: isWin,
|
||||
});
|
||||
}
|
||||
|
||||
/** http.request로 명시 Host 헤더를 보낸다(fetch는 Host 오버라이드를 무시함). */
|
||||
function getWithHost(host) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(
|
||||
{ hostname: "127.0.0.1", port: PORT, path: "/", method: "GET", headers: { Host: host } },
|
||||
(res) => {
|
||||
let body = "";
|
||||
res.on("data", (c) => (body += c));
|
||||
res.on("end", () => resolve({ status: res.statusCode, text: body }));
|
||||
},
|
||||
);
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForUp(timeoutMs = 20000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const { status } = await getWithHost("127.0.0.1");
|
||||
if (status === 200) return true;
|
||||
} catch {
|
||||
// not up yet
|
||||
}
|
||||
await sleep(300);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function killTree(child) {
|
||||
if (!child || child.killed) return;
|
||||
if (isWin && child.pid) {
|
||||
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", shell: true });
|
||||
} else {
|
||||
child.kill("SIGTERM");
|
||||
}
|
||||
}
|
||||
|
||||
let preview;
|
||||
let failed = false;
|
||||
try {
|
||||
preview = startPreview();
|
||||
const up = await waitForUp();
|
||||
if (!up) {
|
||||
console.error("FAIL preview server did not come up; build dist first (npm run build)");
|
||||
failed = true;
|
||||
} else {
|
||||
for (const host of PUBLIC_HOSTS) {
|
||||
const { status, text } = await getWithHost(host);
|
||||
const ok = status === 200 && !text.includes(BLOCK_MARKER) && text.includes("<!DOCTYPE html");
|
||||
console.log(`${ok ? "PASS" : "FAIL"} public host allowed: ${host} (status=${status})`);
|
||||
if (!ok) failed = true;
|
||||
}
|
||||
const neg = await getWithHost(DISALLOWED_HOST);
|
||||
const blocked = neg.text.includes(BLOCK_MARKER);
|
||||
console.log(`${blocked ? "PASS" : "FAIL"} unlisted host blocked: ${DISALLOWED_HOST} (status=${neg.status})`);
|
||||
if (!blocked) failed = true;
|
||||
}
|
||||
} finally {
|
||||
killTree(preview);
|
||||
}
|
||||
console.log(failed ? "FAIL preview host check" : "PASS preview host check");
|
||||
process.exit(failed ? 1 : 0);
|
||||
Loading…
Add table
Add a link
Reference in a new issue