CVE-2026-25699
Description
Apache Answer through 2.0.0 has an authorization bypass in timeline APIs, allowing authenticated users to access private or deleted content.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Answer through 2.0.0 has an authorization bypass in timeline APIs, allowing authenticated users to access private or deleted content.
Vulnerability
Timeline-related APIs in Apache Answer versions through 2.0.0 lacked proper authorization checks. This allowed regular authenticated users to access content that was deleted, private, or unapproved, along with its revision history [1].
Exploitation
An attacker with regular authenticated user privileges can exploit this vulnerability by interacting with the timeline-related APIs. No special network position, write access, or user interaction beyond authentication is required. The vulnerability is present in the API's handling of authorization for accessing historical content [1].
Impact
Successful exploitation allows an authenticated attacker to gain unauthorized access to sensitive information. This includes deleted, private, or unapproved content and its associated revision history, leading to an exposure of private personal information to an unauthorized actor [1].
Mitigation
Users are recommended to upgrade to Apache Answer version 2.0.1, which addresses this issue. The fixed version was released on June 9, 2026 [1].
AI Insight generated on Jun 9, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
192994b49976bfix: add IsAdminModerator field to request structs and implement visibility checks for timeline objects
5 files changed · +123 −7
internal/controller/activity_controller.go+2 −0 modified@@ -58,6 +58,7 @@ func (ac *ActivityController) GetObjectTimeline(ctx *gin.Context) { req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx) if userInfo := middleware.GetUserInfoFromContext(ctx); userInfo != nil { req.IsAdmin = userInfo.RoleID == role.RoleAdminID } @@ -81,6 +82,7 @@ func (ac *ActivityController) GetObjectTimelineDetail(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx) resp, err := ac.activityService.GetObjectTimelineDetail(ctx, req) handler.HandleResponse(ctx, err, resp)
internal/schema/activity.go+9 −7 modified@@ -34,10 +34,11 @@ type ActivityMsg struct { // GetObjectTimelineReq get object timeline request type GetObjectTimelineReq struct { - ObjectID string `validate:"omitempty,gt=0,lte=100" form:"object_id"` - ShowVote bool `validate:"omitempty" form:"show_vote"` - UserID string `json:"-"` - IsAdmin bool `json:"-"` + ObjectID string `validate:"omitempty,gt=0,lte=100" form:"object_id"` + ShowVote bool `validate:"omitempty" form:"show_vote"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` + IsAdminModerator bool `json:"-"` } // GetObjectTimelineResp get object timeline response @@ -73,9 +74,10 @@ type ActObjectInfo struct { // GetObjectTimelineDetailReq get object timeline detail request type GetObjectTimelineDetailReq struct { - NewRevisionID string `validate:"required,gt=0,lte=100" form:"new_revision_id"` - OldRevisionID string `validate:"required,gt=0,lte=100" form:"old_revision_id"` - UserID string `json:"-"` + NewRevisionID string `validate:"required,gt=0,lte=100" form:"new_revision_id"` + OldRevisionID string `validate:"required,gt=0,lte=100" form:"old_revision_id"` + UserID string `json:"-"` + IsAdminModerator bool `json:"-"` } // GetObjectTimelineDetailResp get object timeline detail response
internal/schema/simple_obj_info_schema.go+1 −0 modified@@ -30,6 +30,7 @@ type SimpleObjectInfo struct { ObjectCreatorUserID string `json:"object_creator_user_id"` QuestionID string `json:"question_id"` QuestionStatus int `json:"question_status"` + QuestionShow int `json:"question_show"` AnswerID string `json:"answer_id"` AnswerStatus int `json:"answer_status"` CommentID string `json:"comment_id"`
internal/service/activity/activity.go+108 −0 modified@@ -30,6 +30,7 @@ import ( "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/comment_common" @@ -41,6 +42,7 @@ import ( "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/obj" "github.com/apache/answer/pkg/uid" + "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) @@ -90,6 +92,10 @@ func NewActivityService( // GetObjectTimeline get object timeline func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.GetObjectTimelineReq) ( resp *schema.GetObjectTimelineResp, err error) { + if err = as.ensureTimelineObjectVisible(ctx, req.ObjectID, req.UserID, req.IsAdminModerator); err != nil { + return nil, err + } + resp = &schema.GetObjectTimelineResp{ ObjectInfo: &schema.ActObjectInfo{}, Timeline: make([]*schema.ActObjectTimeline, 0), @@ -254,12 +260,114 @@ func (as *ActivityService) formatTimelineUserInfo(ctx context.Context, timeline // GetObjectTimelineDetail get object timeline func (as *ActivityService) GetObjectTimelineDetail(ctx context.Context, req *schema.GetObjectTimelineDetailReq) ( resp *schema.GetObjectTimelineDetailResp, err error) { + if err = as.ensureTimelineRevisionVisible(ctx, req.NewRevisionID, req.UserID, req.IsAdminModerator); err != nil { + return nil, err + } + if err = as.ensureTimelineRevisionVisible(ctx, req.OldRevisionID, req.UserID, req.IsAdminModerator); err != nil { + return nil, err + } + resp = &schema.GetObjectTimelineDetailResp{} resp.OldRevision, _ = as.getOneObjectDetail(ctx, req.OldRevisionID) resp.NewRevision, _ = as.getOneObjectDetail(ctx, req.NewRevisionID) return resp, nil } +func (as *ActivityService) ensureTimelineRevisionVisible(ctx context.Context, revisionID, userID string, + isAdminModerator bool) error { + if revisionID == "0" { + return nil + } + revisionInfo, err := as.revisionService.GetRevision(ctx, revisionID) + if err != nil { + return err + } + return as.ensureTimelineObjectVisible(ctx, revisionInfo.ObjectID, userID, isAdminModerator) +} + +func (as *ActivityService) ensureTimelineObjectVisible(ctx context.Context, objectID, userID string, + isAdminModerator bool) error { + objInfo, err := as.objectInfoService.GetInfo(ctx, objectID) + if err != nil { + return err + } + + var parentQuestionInfo *schema.SimpleObjectInfo + if objInfo.ObjectType != constant.QuestionObjectType && len(objInfo.QuestionID) > 0 && objInfo.QuestionID != "0" { + parentQuestionInfo, err = as.objectInfoService.GetInfo(ctx, objInfo.QuestionID) + if err != nil { + return err + } + } + + return validateTimelineObjectVisibility(objInfo, parentQuestionInfo, userID, isAdminModerator) +} + +func validateTimelineObjectVisibility(objInfo, parentQuestionInfo *schema.SimpleObjectInfo, + userID string, isAdminModerator bool) error { + if objInfo == nil { + return errors.NotFound(reason.ObjectNotFound) + } + if isTimelineObjectRestricted(objInfo) && + !canViewRestrictedTimelineObject(objInfo.ObjectType, objInfo.ObjectCreatorUserID, userID, isAdminModerator) { + return errors.NotFound(timelineNotFoundReasonByObjectType(objInfo.ObjectType)) + } + if parentQuestionInfo != nil && isTimelineQuestionRestricted(parentQuestionInfo) && + !canViewRestrictedTimelineObject(parentQuestionInfo.ObjectType, parentQuestionInfo.ObjectCreatorUserID, + userID, isAdminModerator) { + return errors.NotFound(reason.QuestionNotFound) + } + return nil +} + +func canViewRestrictedTimelineObject(objectType, creatorUserID, userID string, isAdminModerator bool) bool { + if isAdminModerator { + return true + } + switch objectType { + case constant.QuestionObjectType, constant.AnswerObjectType, constant.CommentObjectType: + return creatorUserID == userID + default: + return false + } +} + +func isTimelineObjectRestricted(objInfo *schema.SimpleObjectInfo) bool { + switch objInfo.ObjectType { + case constant.QuestionObjectType: + return isTimelineQuestionRestricted(objInfo) + case constant.AnswerObjectType: + return objInfo.AnswerStatus == entity.AnswerStatusDeleted || objInfo.AnswerStatus == entity.AnswerStatusPending + case constant.CommentObjectType: + return objInfo.CommentStatus == entity.CommentStatusDeleted || objInfo.CommentStatus == entity.CommentStatusPending + case constant.TagObjectType: + return objInfo.TagStatus == entity.TagStatusDeleted + default: + return false + } +} + +func isTimelineQuestionRestricted(questionInfo *schema.SimpleObjectInfo) bool { + return questionInfo.QuestionStatus == entity.QuestionStatusDeleted || + questionInfo.QuestionStatus == entity.QuestionStatusPending || + questionInfo.QuestionShow == entity.QuestionHide +} + +func timelineNotFoundReasonByObjectType(objectType string) string { + switch objectType { + case constant.QuestionObjectType: + return reason.QuestionNotFound + case constant.AnswerObjectType: + return reason.AnswerNotFound + case constant.CommentObjectType: + return reason.CommentNotFound + case constant.TagObjectType: + return reason.TagNotFound + default: + return reason.ObjectNotFound + } +} + // getOneObjectDetail get object detail func (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID string) ( resp *schema.ObjectTimelineDetail, err error) {
internal/service/object_info/object_info.go+3 −0 modified@@ -204,6 +204,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc ObjectCreatorUserID: questionInfo.UserID, QuestionID: questionInfo.ID, QuestionStatus: questionInfo.Status, + QuestionShow: questionInfo.Show, ObjectType: objectType, Title: questionInfo.Title, Content: questionInfo.ParsedText, // todo trim @@ -228,6 +229,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc ObjectCreatorUserID: answerInfo.UserID, QuestionID: answerInfo.QuestionID, QuestionStatus: questionInfo.Status, + QuestionShow: questionInfo.Show, AnswerStatus: answerInfo.Status, AnswerID: answerInfo.ID, ObjectType: objectType, @@ -258,6 +260,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc if exist { objInfo.QuestionID = questionInfo.ID objInfo.QuestionStatus = questionInfo.Status + objInfo.QuestionShow = questionInfo.Show objInfo.Title = questionInfo.Title } answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, commentInfo.ObjectID)
Vulnerability mechanics
Root cause
"Timeline-related APIs lacked proper authorization checks, allowing unauthorized users to access sensitive content."
Attack vector
An authenticated regular user can craft requests to the timeline APIs to access deleted, private, or unapproved content and its revision history. The vulnerability exists because the `IsAdminModerator` field was not properly checked in the request structs for timeline objects. This allowed users to bypass visibility checks by not having this field set correctly.
Affected code
The vulnerability is in the `GetObjectTimeline` and `GetObjectTimelineDetail` functions within `internal/controller/activity_controller.go`. The core logic for authorization checks was modified in `internal/service/activity/activity.go`, specifically in the `GetObjectTimeline` and `GetObjectTimelineDetail` methods, and new helper functions like `ensureTimelineObjectVisible` and `ensureTimelineRevisionVisible` were added.
What the fix does
The patch introduces the `IsAdminModerator` field to request structs for timeline objects and implements visibility checks. The `ensureTimelineObjectVisible` and `ensureTimelineRevisionVisible` functions now correctly use the `IsAdminModerator` flag to validate if a user has permission to view specific timeline content. This prevents regular authenticated users from accessing restricted or deleted content.
Preconditions
- authThe attacker must be a regular authenticated user.
Generated on Jun 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.