From 4493269479c1baa177160f55ac915ad503d1a924 Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Thu, 16 Jul 2026 23:27:01 +0800 Subject: [PATCH 1/4] chore: add .claude/ to gitignore and untrack it The .claude/ directory holds local Claude Code settings that should not be version-controlled. Add it to .gitignore and remove the already-committed settings from the index (files kept on disk). Co-Authored-By: Claude Fable 5 --- .claude/settings.json | 22 ---------------------- .claude/settings.local.json | 26 -------------------------- .gitignore | 1 + 3 files changed, 1 insertion(+), 48 deletions(-) delete mode 100644 .claude/settings.json delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 3f2e77c..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "permissions": { - "allow": [ - "Read", - "Edit", - "Write", - "Glob", - "Grep", - "Bash(*)", - "WebFetch(*)", - "WebSearch(*)", - "Agent(*)", - "mcp__Claude_Preview__*", - "mcp__Claude_in_Chrome__*", - "mcp__scheduled-tasks__*" - ], - "deny": [ - "Bash(git push --force *)", - "Bash(rm -rf /)" - ] - } -} \ No newline at end of file diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 4f47eff..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "permissions": { - "allow": [ - "Read", - "Edit", - "Write", - "Glob", - "Grep", - "Bash(*)", - "WebFetch(*)", - "WebSearch(*)", - "Agent(*)", - "mcp__Claude_Preview__*", - "mcp__Claude_in_Chrome__*", - "mcp__scheduled-tasks__*", - "Bash(npx vitest:*)", - "Bash(cp \"C:\\\\Users\\\\saopig1\\\\.claude\\\\projects\\\\C--Users-saopig1-Music-teamspeak-music-bot\\\\b5a64d6f-051e-4b87-966c-ece97d2b879b\\\\tool-results\\\\webfetch-1776569008470-exv7qe.bin\" /tmp/design.gz)", - "Bash(gunzip -f /tmp/design.gz)", - "Read(//tmp/**)" - ], - "deny": [ - "Bash(git push --force *)", - "Bash(rm -rf /)" - ] - } -} diff --git a/.gitignore b/.gitignore index 4fef9fa..0cda58a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ config.json cookies/ .superpowers/ .worktrees/ +.claude/ setup.log /bin/ scripts/navbar_bigger.png From ea0f7c17b5983c3b63da4328926abc08162c210e Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Thu, 16 Jul 2026 23:31:31 +0800 Subject: [PATCH 2/4] feat(web): keep deployed WebUI out of search-engine indexes Searching "TsmusicBot" surfaced many deployed instances' WebUI URLs, letting strangers walk into other people's control pages (issue #128). Add defence-in-depth so crawlers stop indexing public deployments: - send `X-Robots-Tag: noindex, nofollow` on every Express response - serve `/robots.txt` with `User-agent: * / Disallow: /` - add `` to index.html, which also covers the /bot/ dedicated-link pages (same SPA shell) These layers only prevent indexing; real protection stays with WebUI auth and the reverse proxy. Document this in the README security section and warn users not to post their WebUI link on public pages. Co-Authored-By: Claude Fable 5 --- README.md | 1 + src/web/robots.test.ts | 86 ++++++++++++++++++++++++++++++++++++++++++ src/web/server.ts | 20 ++++++++-- web/index.html | 6 +++ 4 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/web/robots.test.ts diff --git a/README.md b/README.md index 7731821..4497000 100644 --- a/README.md +++ b/README.md @@ -891,6 +891,7 @@ A:本项目内置 `/login` 限流(每 IP 每分钟 5 次),但生产部 - **会话存储**:服务端 SQLite 表 `sessions`,存储 sha256(token);浏览器只持有原始 token cookie。7 天 TTL,每小时滚动续期。同账号最多 10 个并发会话(超出剔除最旧)。 - **登录限流**:每 IP 每分钟 5 次 `/login` + 3 次 `/setup`,命中返回 429 + `Retry-After`。 - **CSRF & 安全头**:所有 mutating 请求强制 `Origin`/`Referer` 同源;响应携带 `X-Frame-Options: DENY` 和 `Content-Security-Policy: frame-ancestors 'none'`(防点击劫持)。 +- **搜索引擎隐身(防止实例被收录,issue #128)**:为避免部署实例的 WebUI 被搜索引擎收录、被陌生人搜到控制页,采用纵深防御——所有响应携带 `X-Robots-Tag: noindex, nofollow`,`/robots.txt` 返回 `User-agent: * / Disallow: /`,`index.html` 内置 ``(专属链接 `/bot/` 等所有页面同样覆盖)。这些只阻止「被索引」,不是访问控制——**请不要把自己的 WebUI 链接发到公开网页 / 论坛 / 聊天群**,真正的防护来自登录鉴权与反向代理。 - **配置变更**:反向代理部署务必 `"trustProxy": true`(详见 [反向代理部署注意事项](#反向代理部署注意事项))。`config.adminGroups` 现已启用,用于限制管理类聊天命令只能由指定 TeamSpeak 服务器组运行(为空 = 不限制,详见 [TeamSpeak 命令权限](#teamspeak-命令权限管理类命令限制));`config.adminPassword` 仍为旧版预留字段,保留以兼容旧 `config.json`,当前未使用。 ### v0.x — Bot Profile 自动更新与协议层升级 diff --git a/src/web/robots.test.ts b/src/web/robots.test.ts new file mode 100644 index 0000000..9b56079 --- /dev/null +++ b/src/web/robots.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from "vitest"; +import express from "express"; +import request from "supertest"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * Search-engine hardening for issue #128: searching "TsmusicBot" surfaced a + * large number of deployed instances' WebUI URLs, letting strangers walk into + * other people's control pages. The fix is defence in depth — none of these + * layers is authentication (that's handled elsewhere), they just keep the + * public URL out of crawler indexes: + * + * 1. `X-Robots-Tag: noindex, nofollow` on EVERY response; + * 2. `GET /robots.txt` → `User-agent: * / Disallow: /`; + * 3. `` in web/index.html. + * + * The header middleware and the /robots.txt route both live at the top of + * `createWebServer` in `server.ts`; this test asserts the exact behaviour we + * expect from them in isolation (the wiring inside server.ts is verified by + * code review / git diff, matching security-headers.test.ts). + */ +describe("search-engine hardening (issue #128 noindex)", () => { + function buildApp() { + const app = express(); + // Mirrors the security-headers middleware in server.ts. + app.use((_req, res, next) => { + res.setHeader("X-Frame-Options", "DENY"); + res.setHeader("Content-Security-Policy", "frame-ancestors 'none'"); + res.setHeader("X-Robots-Tag", "noindex, nofollow"); + next(); + }); + // Mirrors the public /robots.txt route in server.ts. + app.get("/robots.txt", (_req, res) => { + res.type("text/plain").send("User-agent: *\nDisallow: /\n"); + }); + app.get("/", (_req, res) => res.json({ ok: true })); + app.post("/api/session/login", (_req, res) => res.json({ ok: true })); + return app; + } + + it("sets X-Robots-Tag: noindex, nofollow on GET responses", async () => { + const res = await request(buildApp()).get("/"); + expect(res.status).toBe(200); + expect(res.headers["x-robots-tag"]).toBe("noindex, nofollow"); + }); + + it("sets X-Robots-Tag on POST (API) responses too", async () => { + const res = await request(buildApp()).post("/api/session/login"); + expect(res.headers["x-robots-tag"]).toBe("noindex, nofollow"); + }); + + it("serves /robots.txt disallowing all crawlers", async () => { + const res = await request(buildApp()).get("/robots.txt"); + expect(res.status).toBe(200); + expect(res.headers["content-type"]).toMatch(/text\/plain/); + expect(res.text).toContain("User-agent: *"); + expect(res.text).toContain("Disallow: /"); + }); + + it("still tags the /robots.txt response itself as noindex", async () => { + const res = await request(buildApp()).get("/robots.txt"); + expect(res.headers["x-robots-tag"]).toBe("noindex, nofollow"); + }); +}); + +describe("frontend robots meta tag (issue #128 noindex)", () => { + const indexHtmlPath = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../web/index.html" + ); + const html = fs.readFileSync(indexHtmlPath, "utf-8"); + + const robotsMeta = html.match( + //i + ); + + it("declares a robots meta tag", () => { + expect(robotsMeta).not.toBeNull(); + }); + + it("marks the SPA shell noindex, nofollow (covers /bot/ dedicated links)", () => { + expect(robotsMeta?.[1]).toBe("noindex, nofollow"); + }); +}); diff --git a/src/web/server.ts b/src/web/server.ts index 35d99ff..60bbfd0 100755 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -77,12 +77,19 @@ export function createWebServer(options: WebServerOptions): WebServer { app.set("trust proxy", true); } - // Security headers: prevent the WebUI from being embedded in a third-party - // iframe (clickjacking defence). CSP frame-ancestors is the modern equivalent - // of X-Frame-Options; both are set for compatibility across browsers. + // Security headers: + // • X-Frame-Options / CSP frame-ancestors — prevent the WebUI from being + // embedded in a third-party iframe (clickjacking defence). CSP + // frame-ancestors is the modern equivalent of X-Frame-Options; both are + // set for compatibility across browsers. + // • X-Robots-Tag — keep deployed instances out of search-engine indexes + // (issue #128: searching "TsmusicBot" surfaced strangers' WebUI URLs). + // Set on EVERY response so JSON/API responses and the SPA shell are all + // covered; complements /robots.txt and the tag. app.use((_req, res, next) => { res.setHeader("X-Frame-Options", "DENY"); res.setHeader("Content-Security-Policy", "frame-ancestors 'none'"); + res.setHeader("X-Robots-Tag", "noindex, nofollow"); next(); }); @@ -95,6 +102,13 @@ export function createWebServer(options: WebServerOptions): WebServer { const permissions = createPermissionStore(options.database.db); // ─── Public routes (no auth, no CSRF) ─────────────────────────────────── + // Disallow every crawler (issue #128). Declared before the static SPA + // fallback so this wins over index.html for /robots.txt. Belt-and-braces + // with the X-Robots-Tag header above and the tag. + app.get("/robots.txt", (_req, res) => { + res.type("text/plain").send("User-agent: *\nDisallow: /\n"); + }); + app.get("/api/health", (_req, res) => { res.json({ status: "ok", version: "0.1.0" }); }); diff --git a/web/index.html b/web/index.html index 5c0042c..139121e 100644 --- a/web/index.html +++ b/web/index.html @@ -3,6 +3,12 @@ + +