Browse Source

fix: address critical issues from PR #251 review

1. ReDoS validation gap (security):
   - updateRequestFilterAction now fetches existing filter when matchType
     is not in updates, ensuring regex patterns are always validated
   - Added getRequestFilterById to repository for lookups

2. System settings caching (performance):
   - Added 60s TTL cache for verboseProviderError setting
   - Eliminates redundant DB queries on each failed request

3. Unrelated: Fix import order in chart.tsx (biome auto-fix)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
ding113 3 months ago
parent
commit
b0ddf20bc6

+ 23 - 6
src/actions/request-filters.ts

@@ -9,6 +9,7 @@ import {
   createRequestFilter,
   deleteRequestFilter,
   getAllRequestFilters,
+  getRequestFilterById,
   type RequestFilter,
   type RequestFilterAction,
   type RequestFilterMatchType,
@@ -108,12 +109,28 @@ export async function updateRequestFilterAction(
   const session = await getSession();
   if (!isAdmin(session)) return { ok: false, error: "权限不足" };
 
-  // ReDoS validation: only applies when action is text_replace with regex matchType
-  // Check if we're setting regex pattern that could be unsafe
-  const isTextReplace = updates.action === "text_replace" || updates.action === undefined;
-  const isRegex = updates.matchType === "regex";
-  if (isTextReplace && isRegex && updates.target && !safeRegex(updates.target)) {
-    return { ok: false, error: "正则表达式存在 ReDoS 风险" };
+  // ReDoS validation: applies when action is text_replace with regex matchType
+  // Must check BOTH explicit updates AND existing filter state to prevent bypass
+  if (updates.target) {
+    // Determine effective matchType and action (from updates or existing filter)
+    let effectiveMatchType = updates.matchType;
+    let effectiveAction = updates.action;
+
+    // If matchType or action not in updates, need to check existing filter
+    if (effectiveMatchType === undefined || effectiveAction === undefined) {
+      const existing = await getRequestFilterById(id);
+      if (existing) {
+        if (effectiveMatchType === undefined) effectiveMatchType = existing.matchType;
+        if (effectiveAction === undefined) effectiveAction = existing.action;
+      }
+    }
+
+    const isTextReplace = effectiveAction === "text_replace";
+    const isRegex = effectiveMatchType === "regex";
+
+    if (isTextReplace && isRegex && !safeRegex(updates.target)) {
+      return { ok: false, error: "正则表达式存在 ReDoS 风险" };
+    }
   }
 
   try {

+ 28 - 12
src/app/v1/_lib/proxy/provider-selector.ts

@@ -10,6 +10,32 @@ import type { ClientFormat } from "./format-mapper";
 import { ProxyResponses } from "./responses";
 import type { ProxySession } from "./session";
 
+// 系统设置缓存 - 避免每次请求失败都查询数据库
+const SETTINGS_CACHE_TTL_MS = 60_000; // 60 seconds
+let cachedVerboseProviderError: { value: boolean; expiresAt: number } | null = null;
+
+async function getVerboseProviderErrorCached(): Promise<boolean> {
+  const now = Date.now();
+  if (cachedVerboseProviderError && cachedVerboseProviderError.expiresAt > now) {
+    return cachedVerboseProviderError.value;
+  }
+
+  try {
+    const systemSettings = await getSystemSettings();
+    cachedVerboseProviderError = {
+      value: systemSettings.verboseProviderError,
+      expiresAt: now + SETTINGS_CACHE_TTL_MS,
+    };
+    return systemSettings.verboseProviderError;
+  } catch (e) {
+    logger.warn(
+      "ProviderSelector: Failed to get system settings, using default verboseError=false",
+      { error: e }
+    );
+    return false;
+  }
+}
+
 /**
  * 检查供应商是否支持指定模型(用于调度器匹配)
  *
@@ -329,18 +355,8 @@ export class ProxyProviderResolver {
     // 循环结束:所有可用供应商都已尝试或无可用供应商
     const status = 503;
 
-    // 获取系统设置中的 verboseProviderError 配置
-    // 使用 try/catch 降级保护,避免 DB 异常时导致故障放大
-    let verboseError = false;
-    try {
-      const systemSettings = await getSystemSettings();
-      verboseError = systemSettings.verboseProviderError;
-    } catch (e) {
-      logger.warn(
-        "ProviderSelector: Failed to get system settings, using default verboseError=false",
-        { error: e }
-      );
-    }
+    // 获取系统设置中的 verboseProviderError 配置(使用缓存避免频繁查询数据库)
+    const verboseError = await getVerboseProviderErrorCached();
 
     // 构建详细的错误消息
     let message = "No available providers";

+ 1 - 1
src/components/ui/chart.tsx

@@ -1,8 +1,8 @@
 "use client";
 
 import * as React from "react";
+import type { DefaultLegendContentProps, LegendPayload, TooltipContentProps } from "recharts";
 import * as RechartsPrimitive from "recharts";
-import type { TooltipContentProps, DefaultLegendContentProps, LegendPayload } from "recharts";
 
 import { cn } from "@/lib/utils";
 

+ 11 - 0
src/repository/request-filters.ts

@@ -66,6 +66,17 @@ export async function getAllRequestFilters(): Promise<RequestFilter[]> {
   return rows.map(mapRow);
 }
 
+/**
+ * 根据 ID 获取单个请求过滤器
+ */
+export async function getRequestFilterById(id: number): Promise<RequestFilter | null> {
+  const row = await db.query.requestFilters.findFirst({
+    where: eq(requestFilters.id, id),
+  });
+
+  return row ? mapRow(row) : null;
+}
+
 interface CreateRequestFilterInput {
   name: string;
   description?: string;