VYPR
Unrated severityNVD Advisory· Published Jun 9, 2026· Updated Jun 9, 2026

CVE-2026-25699

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

1

Patches

1
92994b49976b

fix: add IsAdminModerator field to request structs and implement visibility checks for timeline objects

https://github.com/apache/incubator-answerLinkinStarsFeb 6, 2026Fixed in 2.0.1via llm-release-walk
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

2

News mentions

0

No linked articles in our index yet.