mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-16 08:57:30 +02:00
Add files via upload
This commit is contained in:
+54
-6
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user