diff --git a/server/router/api/v1/user_service.go b/server/router/api/v1/user_service.go index e5fcbfd07..04751b251 100644 --- a/server/router/api/v1/user_service.go +++ b/server/router/api/v1/user_service.go @@ -238,6 +238,24 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR case "email": update.Email = &request.User.Email case "avatar_url": + // Validate avatar MIME type to prevent XSS during upload + if request.User.AvatarUrl != "" { + imageType, _, err := extractImageInfo(request.User.AvatarUrl) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid avatar format: %v", err) + } + // Only allow safe image formats for avatars + allowedAvatarTypes := map[string]bool{ + "image/png": true, + "image/jpeg": true, + "image/jpg": true, + "image/gif": true, + "image/webp": true, + } + if !allowedAvatarTypes[imageType] { + return nil, status.Errorf(codes.InvalidArgument, "invalid avatar image type: %s. Only PNG, JPEG, GIF, and WebP are allowed", imageType) + } + } update.AvatarURL = &request.User.AvatarUrl case "description": update.Description = &request.User.Description diff --git a/server/router/frontend/frontend.go b/server/router/frontend/frontend.go index bb60f96ec..1e54ffb6f 100644 --- a/server/router/frontend/frontend.go +++ b/server/router/frontend/frontend.go @@ -35,8 +35,12 @@ func (*FrontendService) Serve(_ context.Context, e *echo.Echo) { if util.HasPrefixes(c.Path(), "/api", "/memos.api.v1") { return true } - // Skip setting cache headers for index.html + // For index.html and root path, set no-cache headers to prevent browser caching + // This prevents sensitive data from being accessible via browser back button after logout if c.Path() == "/" || c.Path() == "/index.html" { + c.Response().Header().Set(echo.HeaderCacheControl, "no-cache, no-store, must-revalidate") + c.Response().Header().Set("Pragma", "no-cache") + c.Response().Header().Set("Expires", "0") return false } // Set Cache-Control header for static assets. diff --git a/web/index.html b/web/index.html index 80133402b..2f65b7630 100644 --- a/web/index.html +++ b/web/index.html @@ -2,6 +2,9 @@
+ + + diff --git a/web/src/components/UserMenu.tsx b/web/src/components/UserMenu.tsx index b7a7cd4f3..0b9026960 100644 --- a/web/src/components/UserMenu.tsx +++ b/web/src/components/UserMenu.tsx @@ -64,7 +64,9 @@ const UserMenu = observer((props: Props) => { keysToRemove.forEach((key) => localStorage.removeItem(key)); - window.location.href = Routes.AUTH; + // Use replace() instead of href to prevent back button from showing cached sensitive data + // This removes the current page from browser history + window.location.replace(Routes.AUTH); }; return ( diff --git a/web/src/connect.ts b/web/src/connect.ts index e2ce276ec..fb426a4d5 100644 --- a/web/src/connect.ts +++ b/web/src/connect.ts @@ -92,7 +92,9 @@ function getAuthFailureRedirect(currentPath: string): string | null { function performRedirect(redirectUrl: string | null): void { if (redirectUrl) { - window.location.href = redirectUrl; + // Use replace() instead of href to prevent back button from showing cached sensitive data + // This removes the current page from browser history after authentication failure + window.location.replace(redirectUrl); } } diff --git a/web/src/layouts/RootLayout.tsx b/web/src/layouts/RootLayout.tsx index 7c3181f90..888f4046d 100644 --- a/web/src/layouts/RootLayout.tsx +++ b/web/src/layouts/RootLayout.tsx @@ -24,12 +24,13 @@ const RootLayout = observer(() => { if (!currentUser) { // If disallowPublicVisibility is enabled, redirect to the login page if the user is not logged in. if (instanceStore.state.memoRelatedSetting.disallowPublicVisibility) { - window.location.href = Routes.AUTH; + // Use replace() to prevent back button from showing cached sensitive data + window.location.replace(Routes.AUTH); return; } else if ( ([Routes.ROOT, Routes.ATTACHMENTS, Routes.INBOX, Routes.ARCHIVED, Routes.SETTING] as string[]).includes(location.pathname) ) { - window.location.href = Routes.EXPLORE; + window.location.replace(Routes.EXPLORE); return; } }