From cc2dff21d2f6cdf9c9bbeb836441b380190db3c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A9=AC=E9=A3=9E?= <13781527+mfcom@user.noreply.gitee.com>
Date: Sat, 30 May 2026 23:19:23 +0800
Subject: [PATCH] =?UTF-8?q?fix(security):=20=E4=BF=AE=E5=A4=8D=20XSS?=
=?UTF-8?q?=E3=80=81CSRF=E3=80=81OAuth=20=E8=B4=A6=E6=88=B7=E6=8E=A5?=
=?UTF-8?q?=E7=AE=A1=E4=B8=89=E4=B8=AA=E9=AB=98=E5=8D=B1=E5=AE=89=E5=85=A8?=
=?UTF-8?q?=E6=BC=8F=E6=B4=9E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. 修复 ContentRender.tsx 存储型 XSS:将 innerHTML 改为 textContent,防止通过项目描述注入恶意脚本窃取 session cookie
2. 添加 CSRF 防护:Session Cookie 设置 SameSite=Lax,新增后端 CSRF 中间件校验 X-Requested-With 头,前端 axios 和 middleware 自动附加
3. 修复 OAuth 用户名冲突账户接管:冲突时拒绝登录而非禁用已有用户
---
.../common/markdown/ContentRender.tsx | 21 +++++++++++++-----
frontend/lib/services/core/api-client.ts | 1 +
frontend/middleware.ts | 1 +
internal/apps/oauth/err.go | 7 +++---
internal/apps/oauth/utils.go | 12 +---------
internal/router/middlewares.go | 22 +++++++++++++++++++
internal/router/router.go | 7 +++---
7 files changed, 49 insertions(+), 22 deletions(-)
diff --git a/frontend/components/common/markdown/ContentRender.tsx b/frontend/components/common/markdown/ContentRender.tsx
index 0c6be110..b13beee4 100644
--- a/frontend/components/common/markdown/ContentRender.tsx
+++ b/frontend/components/common/markdown/ContentRender.tsx
@@ -274,13 +274,24 @@ const markdownComponents: Components = {
const target = e.target as HTMLImageElement;
const errorDiv = document.createElement('div');
errorDiv.className = 'bg-muted border border-border rounded-lg p-4 text-center text-muted-foreground my-6';
- errorDiv.innerHTML = `
+
+ const iconDiv = document.createElement('div');
+ iconDiv.innerHTML = `
-
图片加载失败
- ${alt || src}
- `;
+ `;
+
+ const msgDiv = document.createElement('div');
+ msgDiv.className = 'text-sm';
+ msgDiv.textContent = '图片加载失败';
+
+ const detailDiv = document.createElement('div');
+ detailDiv.className = 'text-xs text-gray-400 dark:text-gray-500 mt-1';
+ detailDiv.textContent = alt || src || '';
+
+ errorDiv.appendChild(iconDiv);
+ errorDiv.appendChild(msgDiv);
+ errorDiv.appendChild(detailDiv);
target.parentNode?.replaceChild(errorDiv, target);
}}
/>
diff --git a/frontend/lib/services/core/api-client.ts b/frontend/lib/services/core/api-client.ts
index 22c98324..761af5a4 100644
--- a/frontend/lib/services/core/api-client.ts
+++ b/frontend/lib/services/core/api-client.ts
@@ -178,6 +178,7 @@ function showRiskBlockedDialog(riskInfo: RiskInfo): void {
apiClient.interceptors.request.use(
(config) => {
config.withCredentials = true;
+ config.headers['X-Requested-With'] = 'XMLHttpRequest';
return config;
},
(error) => Promise.reject(error),
diff --git a/frontend/middleware.ts b/frontend/middleware.ts
index ce819759..046c91e4 100644
--- a/frontend/middleware.ts
+++ b/frontend/middleware.ts
@@ -126,6 +126,7 @@ async function handleReceiveRequest(request: NextRequest, pathname: string): Pro
'User-Agent': 'CDK-Frontend-Middleware',
'Referer': request.headers.get('referer') || '',
'Origin': request.headers.get('origin') || '',
+ 'X-Requested-With': 'XMLHttpRequest',
},
body: Object.keys(backendBody).length > 0 ? JSON.stringify(backendBody) : undefined,
credentials: 'include',
diff --git a/internal/apps/oauth/err.go b/internal/apps/oauth/err.go
index e10d18a6..0e7e6dcd 100644
--- a/internal/apps/oauth/err.go
+++ b/internal/apps/oauth/err.go
@@ -25,7 +25,8 @@
package oauth
const (
- UnAuthorized = "未登录"
- InvalidState = "非法登录请求"
- BannedAccount = "账号已被封禁"
+ UnAuthorized = "未登录"
+ InvalidState = "非法登录请求"
+ BannedAccount = "账号已被封禁"
+ UsernameConflict = "用户名冲突,请稍后重试或联系管理员"
)
diff --git a/internal/apps/oauth/utils.go b/internal/apps/oauth/utils.go
index 91b0a33f..8fa38c19 100644
--- a/internal/apps/oauth/utils.go
+++ b/internal/apps/oauth/utils.go
@@ -31,11 +31,8 @@ import (
"io"
"time"
- "fmt"
-
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
- "github.com/google/uuid"
"github.com/linux-do/cdk/internal/config"
"github.com/linux-do/cdk/internal/db"
"github.com/linux-do/cdk/internal/otel_trace"
@@ -113,14 +110,7 @@ func doOAuth(ctx context.Context, code string) (*User, error) {
err = db.DB(ctx).Transaction(func(tx *gorm.DB) error {
var holder User
if conflictErr := tx.Where("username = ? AND id != ?", userInfo.Username, userInfo.Id).First(&holder).Error; conflictErr == nil {
- // 存在冲突 -> 将占用者改名并注销
- newParams := map[string]interface{}{
- "username": fmt.Sprintf("%s已注销: %s", holder.Username, uuid.NewString()),
- "is_active": false,
- }
- if updateErr := tx.Model(&holder).Updates(newParams).Error; updateErr != nil {
- return updateErr
- }
+ return errors.New(UsernameConflict)
}
// 根据 ID 处理当前用户的 更新 或 创建
diff --git a/internal/router/middlewares.go b/internal/router/middlewares.go
index 1bad4eed..3a322386 100644
--- a/internal/router/middlewares.go
+++ b/internal/router/middlewares.go
@@ -25,7 +25,9 @@
package router
import (
+ "net/http"
"strconv"
+ "strings"
"time"
"github.com/gin-gonic/gin"
@@ -36,6 +38,26 @@ import (
"go.opentelemetry.io/otel/trace"
)
+// csrfMiddleware 校验状态变更请求必须携带 X-Requested-With 头,
+// 防止跨站请求伪造(CSRF)。OAuth callback 和支付回调因来源为外部跳转,放行。
+func csrfMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ method := c.Request.Method
+ if method == "POST" || method == "PUT" || method == "DELETE" || method == "PATCH" {
+ path := c.Request.URL.Path
+ if strings.HasSuffix(path, "/oauth/callback") || strings.HasSuffix(path, "/payment/notify") {
+ c.Next()
+ return
+ }
+ if c.GetHeader("X-Requested-With") != "XMLHttpRequest" {
+ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error_msg": "CSRF 验证失败", "data": nil})
+ return
+ }
+ }
+ c.Next()
+ }
+}
+
func loggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 初始化 Trace
diff --git a/internal/router/router.go b/internal/router/router.go
index 51f100e8..d63cf0d4 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -28,6 +28,7 @@ import (
"context"
"fmt"
"log"
+ "net/http"
"strconv"
"github.com/gin-contrib/sessions"
@@ -78,12 +79,12 @@ func Serve() {
Domain: config.Config.App.SessionDomain,
MaxAge: config.Config.App.SessionAge,
HttpOnly: config.Config.App.SessionHttpOnly,
- Secure: config.Config.App.SessionSecure, // 若用 HTTPS 可以设 true
+ Secure: config.Config.App.SessionSecure,
+ SameSite: http.SameSiteLaxMode,
},
)
r.Use(sessions.Sessions(config.Config.App.SessionCookieName, sessionStore))
-
- // 补充中间件
+ r.Use(csrfMiddleware())
r.Use(otelgin.Middleware(config.Config.App.AppName), loggerMiddleware())
apiGroup := r.Group(config.Config.App.APIPrefix)