fix: security hardening and architectural improvements for custom menu

1. (Critical) Filter admin-only menu items from public API responses -
   both GetPublicSettings handler and GetPublicSettingsForInjection now
   exclude visibility=admin items, preventing unauthorized access to
   admin menu URLs.

2. (Medium) Validate JSON array structure in sanitizeCustomMenuItemsJSON -
   use json.Unmarshal into []json.RawMessage instead of json.Valid to
   reject non-array JSON values that would cause frontend runtime errors.

3. (Medium) Decouple router from business JSON parsing - move origin
   extraction logic from router.go to SettingService.GetFrameSrcOrigins,
   eliminating direct JSON parsing of custom_menu_items in the routing
   layer.

4. (Low) Restrict custom menu item ID charset to [a-zA-Z0-9_-] via
   regex validation, preventing route-breaking characters like / ? # or
   spaces.

5. (Low) Handle crypto/rand error in generateMenuItemID - return error
   instead of silently ignoring, preventing potential duplicate IDs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erio
2026-03-03 07:05:01 +08:00
parent 7541e243bc
commit e97c376681
5 changed files with 150 additions and 66 deletions

View File

@@ -23,11 +23,16 @@ import (
// semverPattern 预编译 semver 格式校验正则
var semverPattern = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
// menuItemIDPattern validates custom menu item IDs: alphanumeric, hyphens, underscores only.
var menuItemIDPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
// generateMenuItemID generates a short random hex ID for a custom menu item.
func generateMenuItemID() string {
func generateMenuItemID() (string, error) {
b := make([]byte, 8)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("generate menu item ID: %w", err)
}
return hex.EncodeToString(b), nil
}
// SettingHandler 系统设置处理器
@@ -358,10 +363,18 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
// Auto-generate ID if missing
if strings.TrimSpace(item.ID) == "" {
items[i].ID = generateMenuItemID()
id, err := generateMenuItemID()
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to generate menu item ID")
return
}
items[i].ID = id
} else if len(item.ID) > maxMenuItemIDLen {
response.BadRequest(c, "Custom menu item ID is too long (max 32 characters)")
return
} else if !menuItemIDPattern.MatchString(item.ID) {
response.BadRequest(c, "Custom menu item ID contains invalid characters (only a-z, A-Z, 0-9, - and _ are allowed)")
return
}
}
// ID uniqueness check