From 04bf772fad72b978cbe178d4024857c422646c31 Mon Sep 17 00:00:00 2001 From: AMAARETS Date: Tue, 28 Oct 2025 19:23:46 +0200 Subject: [PATCH] feat(security): Enhance web application security with origin validation and frame protection - Add middleware to validate request origins based on configuration - Implement Content Security Policy (CSP) for frame ancestors - Update backend routes to support new security middleware - Add configuration options for allowed origins and frame domains - Modify delete message route from GET to POST for better security - Update frontend admin service to use POST for message deletion - Add SameSite cookie policy configuration in session management - Update sample environment configuration with new security settings Improves application security by providing more granular control over request origins, frame embedding, and cookie policies. --- .gitignore | 4 +- SET.md | 8 +++ backend/main.go | 79 +++++++++++++++++++++- backend/settings.go | 19 +++++- frontend/src/app/services/admin.service.ts | 2 +- sample.env | 3 +- 6 files changed, 107 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index c462de3..d4fcc3b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ channel_data/* caddy_config/ data/kvrocks.sock .env -.ds_store \ No newline at end of file +.ds_store +0002-frontend-api-adjustment.txt +0001-backend-security-changes.txt diff --git a/SET.md b/SET.md index 63d9d02..9e5c498 100644 --- a/SET.md +++ b/SET.md @@ -22,6 +22,12 @@ ROOT_STATIC_FOLDER=/path/files במידה ומעוניינים לאפשר צפיה בקבצים רק לרשומים, יש להגדיר בהגדרות הניהול: `require_auth_for_view_files` עם הערך 1. +## אישור טעינת האתר ב - i frame רק לדומיינים מאושרים +במידה וההגדרה `frame_ancestors_domains` מוגדרת בממשק הניהול עם רשימת דומיינים מופרדים ברווח, הטעינה של האתר ב - i frame מאתרים חיצוניים תוגבל לדומיינים האלו בלבד. + +## הגדרת דומיינים מאושרים לגישה לכתובות המאובטחות של האתר +לתוספת אבטחה ניתן להגדיר את ההגדרה `validate_Origin` עם הערך 1 כדי שהאתר יסמוך רק על הדומיין המקורי של האתר לגישה לכתובות מאובטחות באתר למניעת התקפות CSRF. במידה וההגדרה הזו מופעלת ניתן להגדיר דומיינים מאושרים נוספים ע"י ההגדרה `allowed_origins` עם רשימת הדומיינים מופרדים ע"י פסיק. + ## הפעלת כפתור צור קשר במידה וההגדרה `contact_us` מוגדרת בממשק הניהול עם קישור להפניה, יוצג למשתמשים כפתור צור קשר המפנה לקישור. @@ -153,6 +159,8 @@ POST https://example.com/api/import/post |---------------|------|------| |`require_auth` | `1` |חיוב הזדהות בכניסה לערוץ | |`require_auth_for_view_files`|`1`|חיוב הזדהות לצפיה בקבצי תמונות וסרטונים בערוץ| +|`validate_Origin`|`1`|בדיקה של הOrigin ממנו נשלחת הבקשה לדומיין המקורי של האתר או לדומיינים המוגדרים בהגדרה `allowed_origins`| +|`frame_ancestors_domains`|url|רשימת כתובות מופרדת ברווחים של דומיינים המאושרים לטעינת האתר ב - i frame| |`api_secret_key`|`1`|מפתח עבור יבוא הודעות באמצעות API| |`webhook_url`|`https://example.com/webhook`|כתובת לשליחת וובהוק| |`webhook_verify_token`|`your-secret-token`|טוקן לשליחה יחד עם וובהוק| diff --git a/backend/main.go b/backend/main.go index 5b1fb8d..2022ac5 100644 --- a/backend/main.go +++ b/backend/main.go @@ -5,6 +5,7 @@ import ( "encoding/gob" "log" "net/http" + "net/url" _ "net/http/pprof" "os" "path/filepath" @@ -12,6 +13,7 @@ import ( "github.com/boj/redistore" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" + "github.com/gorilla/sessions" ) var rootStaticFolder = os.Getenv("ROOT_STATIC_FOLDER") @@ -37,6 +39,62 @@ func ifRequireAuth(next http.Handler) http.Handler { }) } +func cspFrameAncestorsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if settingConfig.FrameAncestorsDomains != "" { + cspValue := "frame-ancestors 'self'" + cspValue = cspValue + " " + settingConfig.FrameAncestorsDomains + w.Header().Set("Content-Security-Policy", cspValue) + } + next.ServeHTTP(w, r) + }) +} + +func validateOrigin(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + + if !settingConfig.validateOrigin { + next.ServeHTTP(w, r) + return + } + + if origin == "" { + next.ServeHTTP(w, r) + return + } + + originURL, err := url.Parse(origin) + if err != nil { + http.Error(w, "Forbidden: Malformed Origin Header", http.StatusForbidden) + return + } + + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + appHostOrigin := scheme + "://" + r.Host + + allowed := make(map[string]struct{}) + + allowed[appHostOrigin] = struct{}{} + + if len(settingConfig.AllowedOrigins) > 0 { + for _, allowedOrigin := range settingConfig.AllowedOrigins { + allowed[allowedOrigin] = struct{}{} + } + } + + if _, ok := allowed[originURL.String()]; !ok { + http.Error(w, "Forbidden: Invalid Origin", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} + func main() { gob.Register(Session{}) initializePrivilegeUsers() @@ -48,11 +106,27 @@ func main() { panic(err) } store.SetMaxAge(60 * 60 * 24 * 30) - store.Options.HttpOnly = true + store.Options = &sessions.Options{ + Path: "/", + HttpOnly: true, + } + + sameSitePolicy := os.Getenv("COOKIE_SAMESITE_POLICY") + + switch sameSitePolicy { + case "None": + store.Options.SameSite = http.SameSiteNoneMode + store.Options.Secure = true + case "Strict": + store.Options.SameSite = http.SameSiteStrictMode + default: + store.Options.SameSite = http.SameSiteLaxMode + } defer store.Close() r := chi.NewRouter() r.Use(middleware.Logger) + r.Use(cspFrameAncestorsMiddleware) // Protected with api key r.Post("/api/import/post", addNewPost) @@ -86,10 +160,11 @@ func main() { api.Route("/admin", func(protected chi.Router) { // ⚠️ WARNING: Route not check privilege use protectedWithPrivilege to check privilege. + protected.Use(validateOrigin) protected.Post("/new", protectedWithPrivilege(Writer, addMessage)) protected.Post("/edit-message", protectedWithPrivilege(Writer, updateMessage)) - protected.Get("/delete-message/{id}", protectedWithPrivilege(Writer, deleteMessage)) + protected.Post("/delete-message/{id}", protectedWithPrivilege(Writer, deleteMessage)) protected.Post("/upload", protectedWithPrivilege(Writer, uploadFile)) protected.Post("/edit-channel-info", protectedWithPrivilege(Moderator, editChannelInfo)) protected.Get("/statistics", protectedWithPrivilege(Moderator, getStatistics)) diff --git a/backend/settings.go b/backend/settings.go index c9286a0..f7dba67 100644 --- a/backend/settings.go +++ b/backend/settings.go @@ -41,6 +41,9 @@ type SettingConfig struct { MaxFileSize int64 CustomTitle string ContactUs string + FrameAncestorsDomains string + AllowedOrigins []string + validateOrigin bool } type Setting struct { @@ -184,8 +187,18 @@ func (s *Settings) ToConfig() *SettingConfig { case "fcm_json_universe_domain": config.FcmJson.UniverseDomain = setting.GetString() - case "contact_us": - config.ContactUs = setting.GetString() + case "frame_ancestors_domains": + config.FrameAncestorsDomains = setting.GetString() + case "allowed_origins": + originsStr := setting.GetString() + if originsStr != "" { + origins := strings.Split(originsStr, ",") + for _, origin := range origins { + config.AllowedOrigins = append(config.AllowedOrigins, strings.TrimSpace(origin)) + } + } + case "validate_Origin": + config.validateOrigin = setting.GetBool() } } @@ -197,7 +210,7 @@ func (s *Setting) GetBool() bool { return b } -func (s *Setting) GetString() string { +func (s *Setting) GetString() string { str, _ := dyno.GetString(s.Value) return str } diff --git a/frontend/src/app/services/admin.service.ts b/frontend/src/app/services/admin.service.ts index 67d8afb..6f14193 100644 --- a/frontend/src/app/services/admin.service.ts +++ b/frontend/src/app/services/admin.service.ts @@ -56,7 +56,7 @@ export class AdminService { } deleteMessage(id: number | undefined): Observable { - return this.http.get(`/api/admin/delete-message/${id}`); + return this.http.post(`/api/admin/delete-message/${id}` ,null); } uploadFile(formData: FormData) { diff --git a/sample.env b/sample.env index 04e4212..43ed6b0 100644 --- a/sample.env +++ b/sample.env @@ -7,4 +7,5 @@ REDIS_PROTOCOL=unix # Google-oauth2 GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret -ADMIN_USERS=example@gmail.com,example1@gmail.com \ No newline at end of file +ADMIN_USERS=example@gmail.com,example1@gmail.com +COOKIE_SAMESITE_POLICY=None