wechat-robot-skills/skills/web-page/scripts/web_page.test.ts

182 lines
5.7 KiB
TypeScript

import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { once } from "node:events";
import http from "node:http";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { isLocalFileUrl, validateUrl } from "./web_page.ts";
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT_PATH = path.join(SCRIPT_DIR, "web_page.ts");
const PASSWD_MARKER = "root:x:0:0";
interface ScriptResult {
code: number | null;
stdout: string;
stderr: string;
}
function runWebPage(url: string, args: string[] = []): Promise<ScriptResult> {
return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[
"--experimental-strip-types",
SCRIPT_PATH,
"--url",
url,
"--mode",
"content",
"--wait_ms",
"300",
"--timeout_ms",
"8000",
"--max_chars",
"4000",
...args,
],
{
cwd: SCRIPT_DIR,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
},
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk: Buffer) => {
stdout += chunk.toString("utf8");
});
child.stderr.on("data", (chunk: Buffer) => {
stderr += chunk.toString("utf8");
});
child.once("error", reject);
const timer = setTimeout(() => {
child.kill("SIGKILL");
reject(new Error(`web_page.ts 执行超时\nstdout: ${stdout}\nstderr: ${stderr}`));
}, 20000);
child.once("close", (code) => {
clearTimeout(timer);
resolve({ code, stdout, stderr });
});
});
}
function assertLocalFileBlocked(result: ScriptResult): void {
const output = `${result.stdout}\n${result.stderr}`;
assert.notEqual(result.code, 0, output);
assert.match(
output,
/已阻止浏览器|网页链接必须是 http 或 https 地址|网页导航失败/,
);
assert.doesNotMatch(output, new RegExp(PASSWD_MARKER));
}
test("web-page 本地文件访问防护", async (t) => {
const server = http.createServer((request, response) => {
const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
response.setHeader("Content-Type", "text/html; charset=utf-8");
switch (requestUrl.pathname) {
case "/redirect-file":
response.statusCode = 302;
response.setHeader("Location", "file:///etc/passwd");
response.end();
return;
case "/click-file":
response.end(
'<!doctype html><title>click file</title><a id="local-file" href="file:///etc/passwd">打开本地文件</a>',
);
return;
case "/js-location":
response.end(
'<!doctype html><title>js location</title><p>safe</p><script>setTimeout(() => { location.href = "file:///etc/passwd"; }, 50);</script>',
);
return;
case "/iframe-file":
response.end(
'<!doctype html><title>iframe file</title><p>safe</p><iframe src="file:///etc/passwd"></iframe>',
);
return;
case "/popup-file":
response.end(
'<!doctype html><title>popup file</title><button id="open-popup" onclick="window.open(\'file:///etc/passwd\', \'_blank\')">打开弹窗</button>',
);
return;
case "/redirect-http":
response.statusCode = 302;
response.setHeader("Location", "/search?q=normal-http-redirect");
response.end();
return;
case "/search":
response.end(
`<!doctype html><title>search ok</title><main>SEARCH_OK:${requestUrl.searchParams.get("q") || ""}</main><script>console.log("file:///not-an-access-attempt")</script>`,
);
return;
default:
response.statusCode = 404;
response.end("not found");
}
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
t.after(() => server.close());
const address = server.address();
assert.ok(address && typeof address === "object");
const baseUrl = `http://127.0.0.1:${address.port}`;
await t.test("直接访问 file:///etc/passwd", async () => {
assertLocalFileBlocked(await runWebPage("file:///etc/passwd"));
});
await t.test("HTTP 302 跳转到 file://", async () => {
assertLocalFileBlocked(await runWebPage(`${baseUrl}/redirect-file`));
});
await t.test("点击 file:// 链接", async () => {
assertLocalFileBlocked(
await runWebPage(`${baseUrl}/click-file`, [
"--actions",
JSON.stringify([{ type: "click", selector: "#local-file" }]),
]),
);
});
await t.test("JavaScript 修改 location", async () => {
assertLocalFileBlocked(await runWebPage(`${baseUrl}/js-location`));
});
await t.test("iframe 加载本地文件", async () => {
assertLocalFileBlocked(await runWebPage(`${baseUrl}/iframe-file`));
});
await t.test("弹窗加载本地文件", async () => {
assertLocalFileBlocked(
await runWebPage(`${baseUrl}/popup-file`, [
"--actions",
JSON.stringify([{ type: "click", selector: "#open-popup" }]),
]),
);
});
await t.test("正常 HTTP/HTTPS 搜索及浏览不受影响", async () => {
assert.equal(
validateUrl("https://example.com/search?q=normal-https"),
"https://example.com/search?q=normal-https",
);
assert.equal(isLocalFileUrl("https://example.com/file.txt"), false);
assert.equal(isLocalFileUrl("file:///etc/passwd"), true);
assert.equal(isLocalFileUrl("filesystem:https://example.com/temporary/a"), true);
const normal = await runWebPage(`${baseUrl}/redirect-http`);
assert.equal(normal.code, 0, `${normal.stdout}\n${normal.stderr}`);
assert.match(normal.stdout, /SEARCH_OK:normal-http-redirect/);
assert.doesNotMatch(normal.stdout, new RegExp(PASSWD_MARKER));
});
});