fix: security hardening on synced upstream codebase

Applied security fixes to the latest upstream (usememos/memos):

- Remove hardcoded JWT secret ("usememos") in demo mode; always use instance secret key
- Enforce DisallowPasswordAuth for all roles including admins (was only blocking regular users)
- Add minimum password length validation (8 chars) on CreateUser and UpdateUser password change
- Restrict CORS to same-origin in production (was allowing all origins on both gateway and connect)
- Add HTTP client timeout (10s) to OAuth2 identity provider
- Remove PII logging of OAuth2 user info claims

https://claude.ai/code/session_018iYDVMmBxJLWBvqugc6tNe
This commit is contained in:
Claude 2026-03-15 16:51:53 +00:00
parent b8e9ee2b26
commit d88a116fbc
No known key found for this signature in database
5 changed files with 29 additions and 13 deletions

View File

@ -6,8 +6,8 @@ import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"time"
"github.com/pkg/errors"
"golang.org/x/oauth2"
@ -79,7 +79,7 @@ func (p *IdentityProvider) ExchangeToken(ctx context.Context, redirectURL, code,
// UserInfo returns the parsed user information using the given OAuth2 token.
func (p *IdentityProvider) UserInfo(token string) (*idp.IdentityProviderUserInfo, error) {
client := &http.Client{}
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest(http.MethodGet, p.config.UserInfoUrl, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to create http request")
@ -101,7 +101,6 @@ func (p *IdentityProvider) UserInfo(token string) (*idp.IdentityProviderUserInfo
if err := json.Unmarshal(body, &claims); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal response body")
}
slog.Info("user info claims", "claims", claims)
userInfo := &idp.IdentityProviderUserInfo{}
if v, ok := claims[p.config.FieldMapping.Identifier].(string); ok {
userInfo.Identifier = v
@ -129,6 +128,5 @@ func (p *IdentityProvider) UserInfo(token string) (*idp.IdentityProviderUserInfo
userInfo.AvatarURL = v
}
}
slog.Info("user info", "userInfo", userInfo)
return userInfo, nil
}

View File

@ -83,8 +83,8 @@ func (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get instance general setting, error: %v", err)
}
// Check if the password auth in is allowed.
if instanceGeneralSetting.DisallowPasswordAuth && user.Role == store.RoleUser {
// Check if password auth is allowed. Enforce for all roles including admins.
if instanceGeneralSetting.DisallowPasswordAuth {
return nil, status.Errorf(codes.PermissionDenied, "password signin is not allowed")
}
existingUser = user

View File

@ -160,6 +160,9 @@ func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserR
}, nil
}
if len(request.User.Password) < 8 {
return nil, status.Errorf(codes.InvalidArgument, "password must be at least 8 characters")
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate password hash: %v", err)
@ -269,6 +272,9 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
role := convertUserRoleToStore(request.User.Role)
update.Role = &role
case "password":
if len(request.User.Password) < 8 {
return nil, status.Errorf(codes.InvalidArgument, "password must be at least 8 characters")
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate password hash: %v", err)

View File

@ -115,7 +115,17 @@ func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Ech
}
gwGroup := echoServer.Group("")
gwGroup.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
UnsafeAllowOriginFunc: func(_ *echo.Context, origin string) (string, bool, error) {
// In demo mode, allow all origins for development convenience.
// In production, deny cross-origin requests (same-origin only).
if s.Profile.Demo {
return origin, true, nil
}
return "", false, nil
},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodOptions},
AllowHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
}))
// Register SSE endpoint with same CORS as rest of /api/v1.
gwGroup.GET("/api/v1/sse", func(c *echo.Context) error {
@ -141,7 +151,11 @@ func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Ech
// Wrap with CORS for browser access
corsHandler := middleware.CORSWithConfig(middleware.CORSConfig{
UnsafeAllowOriginFunc: func(_ *echo.Context, origin string) (string, bool, error) {
return origin, true, nil
// In demo mode, allow all origins for development convenience.
if s.Profile.Demo {
return origin, true, nil
}
return "", false, nil
},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodOptions},
AllowHeaders: []string{"*"},

View File

@ -50,11 +50,9 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
return nil, errors.Wrap(err, "failed to get instance basic setting")
}
secret := "usememos"
if !profile.Demo {
secret = instanceBasicSetting.SecretKey
}
s.Secret = secret
// Always use the instance secret key, regardless of mode.
// Never fall back to a hardcoded secret, as it allows token forgery.
s.Secret = instanceBasicSetting.SecretKey
// Register healthz endpoint.
echoServer.GET("/healthz", func(c *echo.Context) error {