|
|
@@ -9,6 +9,43 @@ import {
|
|
|
type UpdateNotificationSettingsInput,
|
|
|
} from "@/repository/notifications";
|
|
|
|
|
|
+/**
|
|
|
+ * 检查 URL 是否指向内部/私有网络(SSRF 防护)
|
|
|
+ */
|
|
|
+function isInternalUrl(urlString: string): boolean {
|
|
|
+ try {
|
|
|
+ const url = new URL(urlString);
|
|
|
+ const hostname = url.hostname.toLowerCase();
|
|
|
+
|
|
|
+ // 阻止 localhost 和 IPv6 loopback
|
|
|
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析 IPv4 地址
|
|
|
+ const ipv4Match = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
|
+ if (ipv4Match) {
|
|
|
+ const [, a, b, c] = ipv4Match.map(Number);
|
|
|
+ // 私有 IP 范围
|
|
|
+ if (a === 10) return true; // 10.0.0.0/8
|
|
|
+ if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
|
|
|
+ if (a === 192 && b === 168) return true; // 192.168.0.0/16
|
|
|
+ if (a === 169 && b === 254) return true; // 169.254.0.0/16 (link-local)
|
|
|
+ if (a === 0) return true; // 0.0.0.0/8
|
|
|
+ }
|
|
|
+
|
|
|
+ // 危险端口
|
|
|
+ const dangerousPorts = [22, 23, 3306, 5432, 27017, 6379, 11211];
|
|
|
+ if (url.port && dangerousPorts.includes(parseInt(url.port, 10))) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ } catch {
|
|
|
+ return true; // 无效 URL 视为不安全
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 获取通知设置
|
|
|
*/
|
|
|
@@ -57,8 +94,20 @@ export async function testWebhookAction(
|
|
|
return { success: false, error: "Webhook URL 不能为空" };
|
|
|
}
|
|
|
|
|
|
+ const trimmedUrl = webhookUrl.trim();
|
|
|
+
|
|
|
+ // SSRF 防护: 阻止访问内部网络
|
|
|
+ if (isInternalUrl(trimmedUrl)) {
|
|
|
+ logger.warn({
|
|
|
+ action: "webhook_test_blocked",
|
|
|
+ reason: "internal_url",
|
|
|
+ url: trimmedUrl.replace(/key=[^&]+/, "key=***"), // 脱敏
|
|
|
+ });
|
|
|
+ return { success: false, error: "不允许访问内部网络地址" };
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
- const bot = new WeChatBot(webhookUrl.trim());
|
|
|
+ const bot = new WeChatBot(trimmedUrl);
|
|
|
const result = await bot.testConnection();
|
|
|
|
|
|
return result;
|