mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea0f7c17b5 | ||
|
|
051171b019 |
No files matched your search
@@ -15,8 +15,6 @@
|
||||
"mcp__scheduled-tasks__*"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(git push * main)",
|
||||
"Bash(git push * master)",
|
||||
"Bash(git push --force *)",
|
||||
"Bash(rm -rf /)"
|
||||
]
|
||||
|
||||
@@ -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` 内置 `<meta name="robots" content="noindex, nofollow">`(专属链接 `/bot/<id>` 等所有页面同样覆盖)。这些只阻止「被索引」,不是访问控制——**请不要把自己的 WebUI 链接发到公开网页 / 论坛 / 聊天群**,真正的防护来自登录鉴权与反向代理。
|
||||
- **配置变更**:反向代理部署务必 `"trustProxy": true`(详见 [反向代理部署注意事项](#反向代理部署注意事项))。`config.adminGroups` 现已启用,用于限制管理类聊天命令只能由指定 TeamSpeak 服务器组运行(为空 = 不限制,详见 [TeamSpeak 命令权限](#teamspeak-命令权限管理类命令限制));`config.adminPassword` 仍为旧版预留字段,保留以兼容旧 `config.json`,当前未使用。
|
||||
|
||||
### v0.x — Bot Profile 自动更新与协议层升级
|
||||
|
||||
@@ -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. `<meta name="robots" content="noindex, nofollow">` 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(
|
||||
/<meta\s+name=["']robots["']\s+content=["']([^"']+)["']\s*\/?>/i
|
||||
);
|
||||
|
||||
it("declares a robots meta tag", () => {
|
||||
expect(robotsMeta).not.toBeNull();
|
||||
});
|
||||
|
||||
it("marks the SPA shell noindex, nofollow (covers /bot/<id> dedicated links)", () => {
|
||||
expect(robotsMeta?.[1]).toBe("noindex, nofollow");
|
||||
});
|
||||
});
|
||||
+17
-3
@@ -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 <meta name="robots"> 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 <meta name="robots"> 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" });
|
||||
});
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Keep deployed instances out of search-engine indexes (issue #128:
|
||||
searching "TsmusicBot" surfaced strangers' WebUI URLs). Defence in depth
|
||||
alongside the server's X-Robots-Tag header and /robots.txt. Applies to
|
||||
the SPA shell and every in-app route (incl. /bot/<id> dedicated links),
|
||||
since they all share this single index.html. -->
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<!-- Bilibili / NetEase / QQ image CDNs reject requests whose Referer is not on
|
||||
their whitelist, so we must not leak a Referer cross-origin. "same-origin"
|
||||
does exactly that: full Referer for our own requests, none for cross-origin
|
||||
|
||||
Reference in new issue
Block a user