CVE-2026-34905
Description
Apache Answer through 2.0.0 allows authenticated users to access unlisted questions and their associated data via direct API endpoints.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Answer through 2.0.0 allows authenticated users to access unlisted questions and their associated data via direct API endpoints.
Vulnerability
An Exposure of Sensitive Information to an Unauthorized Actor vulnerability exists in Apache Answer through version 2.0.0. The unlisted question feature failed to enforce access restrictions on direct API endpoints, enabling authenticated users to discover and access unlisted questions, their answers, comments, and revision history.
Exploitation
An attacker who is already authenticated to Apache Answer can exploit this vulnerability by directly accessing API endpoints that were not intended for public use. This allows them to bypass access controls and retrieve sensitive information related to unlisted questions.
Impact
Successful exploitation allows an authenticated attacker to gain unauthorized access to sensitive information, including unlisted questions, their answers, comments, and revision history. This constitutes an information disclosure vulnerability.
Mitigation
Apache has released version 2.0.1, which addresses this issue. Users are recommended to upgrade to version 2.0.1 or later to fix the vulnerability. The release date for version 2.0.1 is not yet disclosed in the available references [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
2Patches
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
"The unlisted question feature did not enforce access restrictions on direct API endpoints, allowing authenticated users to discover and access unlisted questions, their answers, comments, and revision history."
Attack vector
An authenticated user could directly access API endpoints related to unlisted questions. This bypasses intended access controls, enabling the user to view sensitive information such as answers, comments, and revision history associated with questions they should not have access to. The vulnerability stems from a lack of proper authorization checks on these specific API endpoints [patch_id=5343576].
Affected code
The vulnerability is addressed in the `internal/service/activity/activity.go` file, specifically within the `GetObjectTimeline` and `GetObjectTimelineDetail` functions. The changes also affect `internal/schema/activity.go` by adding the `IsAdminModerator` field to request structs, and `internal/controller/activity_controller.go` to pass this new field.
What the fix does
The patch introduces visibility checks for timeline objects, including questions, answers, and comments. It adds an `IsAdminModerator` field to request structs and implements new functions like `ensureTimelineObjectVisible` and `validateTimelineObjectVisibility`. These functions verify if a user, based on their role and ownership, is permitted to view restricted timeline objects, thereby closing the access loophole [patch_id=5343576].
Preconditions
- authThe attacker must be an 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.