Add files via upload

This commit is contained in:
公明
2026-07-14 11:52:27 +08:00
committed by GitHub
parent 1be10cdf2c
commit 7d1e16b97b
3 changed files with 129 additions and 7 deletions
+54 -6
View File
@@ -84,7 +84,7 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
router := gin.Default()
// CORS中间件
router.Use(corsMiddleware())
router.Use(corsMiddleware(cfg.Server.CORSAllowedOrigins))
// 初始化数据库
dbPath := cfg.Database.Path
@@ -2069,22 +2069,36 @@ func initializeKnowledge(
return knowledgeHandler, nil
}
// corsMiddleware CORS中间件
func corsMiddleware() gin.HandlerFunc {
// corsMiddleware allows same-origin requests, valid Chromium extension
// origins, and exact origins explicitly configured by the operator. CORS is
// not an authentication boundary; API access still requires a valid session.
func corsMiddleware(configuredOrigins []string) gin.HandlerFunc {
allowedOrigins := make(map[string]struct{}, len(configuredOrigins))
for _, origin := range configuredOrigins {
if normalized, ok := normalizeCORSOrigin(origin); ok {
allowedOrigins[normalized] = struct{}{}
}
}
return func(c *gin.Context) {
origin := strings.TrimSpace(c.GetHeader("Origin"))
if origin != "" {
parsed, err := url.Parse(origin)
if err != nil || parsed.Host == "" || !strings.EqualFold(parsed.Host, c.Request.Host) {
c.Writer.Header().Add("Vary", "Origin")
normalized, valid := normalizeCORSOrigin(origin)
_, explicitlyAllowed := allowedOrigins[normalized]
parsed, _ := url.Parse(origin)
sameHost := valid && strings.EqualFold(parsed.Host, c.Request.Host)
browserExtension := valid && isChromiumExtensionOrigin(parsed)
if !sameHost && !browserExtension && !explicitlyAllowed {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "cross-origin request denied"})
return
}
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Add("Vary", "Origin")
}
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
c.Writer.Header().Set("Access-Control-Max-Age", "600")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
@@ -2094,3 +2108,37 @@ func corsMiddleware() gin.HandlerFunc {
c.Next()
}
}
// isChromiumExtensionOrigin accepts only Chrome's canonical 32-character
// extension IDs (letters a-p). It does not allow arbitrary custom schemes or
// web origins, and the extension must separately obtain host permission.
func isChromiumExtensionOrigin(origin *url.URL) bool {
if origin == nil || !strings.EqualFold(origin.Scheme, "chrome-extension") || origin.Port() != "" {
return false
}
id := strings.ToLower(origin.Hostname())
if len(id) != 32 {
return false
}
for _, ch := range id {
if ch < 'a' || ch > 'p' {
return false
}
}
return true
}
// normalizeCORSOrigin validates and canonicalizes a serialized origin. CORS
// origins never contain credentials, paths, query strings, or fragments.
func normalizeCORSOrigin(raw string) (string, bool) {
raw = strings.TrimSpace(raw)
if raw == "" || raw == "*" || strings.EqualFold(raw, "null") {
return "", false
}
parsed, err := url.Parse(raw)
if err != nil || parsed.Scheme == "" || parsed.Host == "" || parsed.User != nil ||
(parsed.Path != "" && parsed.Path != "/") || parsed.RawQuery != "" || parsed.Fragment != "" {
return "", false
}
return strings.ToLower(parsed.Scheme) + "://" + strings.ToLower(parsed.Host), true
}
+72 -1
View File
@@ -11,7 +11,7 @@ import (
func TestCORSMiddlewareAllowsSameOriginAndRejectsForeignOrigin(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(corsMiddleware())
router.Use(corsMiddleware(nil))
router.GET("/test", func(c *gin.Context) { c.Status(http.StatusNoContent) })
same := httptest.NewRequest(http.MethodGet, "http://app.example/test", nil)
@@ -32,3 +32,74 @@ func TestCORSMiddlewareAllowsSameOriginAndRejectsForeignOrigin(t *testing.T) {
t.Fatalf("foreign-origin response = %d, want %d", foreignW.Code, http.StatusForbidden)
}
}
func TestCORSMiddlewareAllowsBrowserExtensionWithoutConfiguration(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(corsMiddleware(nil))
router.POST("/api/auth/login", func(c *gin.Context) { c.Status(http.StatusNoContent) })
req := httptest.NewRequest(http.MethodOptions, "https://server.example/api/auth/login", nil)
req.Host = "server.example"
req.Header.Set("Origin", "chrome-extension://abcdefghijklmnopabcdefghijklmnop")
req.Header.Set("Access-Control-Request-Method", http.MethodPost)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNoContent {
t.Fatalf("preflight response = %d, want %d", w.Code, http.StatusNoContent)
}
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "chrome-extension://abcdefghijklmnopabcdefghijklmnop" {
t.Fatalf("allow-origin = %q", got)
}
}
func TestCORSMiddlewareRejectsInvalidExtensionOrigins(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, origin := range []string{
"chrome-extension://too-short",
"chrome-extension://qrstuvwxyzabcdefqrstuvwxyzabcdef",
"chrome-extension://abcdefghijklmnopabcdefghijklmnop:8443",
"moz-extension://abcdefghijklmnopabcdefghijklmnop",
} {
t.Run(origin, func(t *testing.T) {
router := gin.New()
router.Use(corsMiddleware(nil))
router.GET("/test", func(c *gin.Context) { c.Status(http.StatusNoContent) })
req := httptest.NewRequest(http.MethodGet, "https://server.example/test", nil)
req.Host = "server.example"
req.Header.Set("Origin", origin)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Fatalf("response = %d, want %d", w.Code, http.StatusForbidden)
}
})
}
}
func TestCORSMiddlewareRejectsUnsafeConfiguredEntries(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, configured := range []string{
"*",
"null",
"https://trusted.example/extra",
"https://trusted.example?trusted=true",
} {
t.Run(configured, func(t *testing.T) {
router := gin.New()
router.Use(corsMiddleware([]string{configured}))
router.GET("/test", func(c *gin.Context) { c.Status(http.StatusNoContent) })
req := httptest.NewRequest(http.MethodGet, "https://server.example/test", nil)
req.Host = "server.example"
req.Header.Set("Origin", "https://trusted.example")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Fatalf("response = %d, want %d", w.Code, http.StatusForbidden)
}
})
}
}