fix(mailer): reject incoming email tokens of users who cannot sign in (#39833) (#39841)

Backport #39833 by @basavaraj-sm05

Incoming email reply tokens stay valid for a year and are verified with
`user.Rands`, which is not rotated when an account is deactivated or
prohibited from signing in. A suspended user could therefore keep
commenting on public repos by replying to old notifications.

`DecodeToken` now rejects tokens of organizations, inactive users and
users prohibited from signing in, which covers every incoming mail
handler.

Co-authored-by: Basavaraj S m <basavaraj@digiscrypt.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-18 12:28:08 +00:00
1 parent dc30660291
commit 1eee73e9bd
3 files changed
+63 -3

No files matched your search

+28 -2
View File
@@ -1021,7 +1021,8 @@ func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, er
Find(&users)
}
// GetUserByID returns the user object by given ID if exists.
// GetUserByID returns the user object by given ID if exists. TODO: for most cases, this function should be avoided.
// Use correct GetXxxUser functions instead to handle user types & states correctly.
func GetUserByID(ctx context.Context, id int64) (*User, error) {
u := new(User)
has, err := db.GetEngine(ctx).ID(id).Get(u)
@@ -1033,7 +1034,8 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) {
return u, nil
}
// GetUserByIDs returns the user objects by given IDs if exists.
// GetUserByIDs returns the user objects by given IDs if exists. TODO: for most cases, this function should be avoided.
// Use correct GetXxxUser functions instead to handle user types & states correctly.
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
if len(ids) == 0 {
return nil, nil
@@ -1046,6 +1048,13 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
return users, err
}
// Glossaries for XxxUser:
// * Possible: The user may or may not exist in the database, but we can still return a valid user object (e.g. Ghost user) for the given ID.
// * Individual: The user is a real person, not an organization or a bot.
// * Doer: The user is the one performing an action
// * Valid: The user is an active user and not prohibited from signing in
// * System: Negative ID (e.g. Ghost, Actions, DeployKey, etc.) and not a real person, Ghost is not included if there is no "Possible" keyword
// GetPossibleUserByID returns the possible user and its ID. If the user doesn't exist, it returns Ghost user
func GetPossibleUserByID(ctx context.Context, id int64) (_ int64, u *User, err error) {
if id < 0 {
@@ -1082,6 +1091,23 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
return users, nil
}
func GetValidDoerUserByID(ctx context.Context, id int64) (*User, error) {
u, err := GetUserByID(ctx, id)
if err != nil {
return nil, err
}
if !u.IsIndividual() && !u.IsTypeBot() {
return nil, util.NewNotExistErrorf("user %d is not a doer", id)
}
if !u.IsActive {
return nil, util.NewPermissionDeniedErrorf("user %d is inactive", id)
}
if u.ProhibitLogin {
return nil, util.NewPermissionDeniedErrorf("user %d is prohibited from login", id)
}
return u, nil
}
func getUserByNameWithTypes(ctx context.Context, name string, types ...UserType) (*User, error) {
u := &User{}
sess := db.GetEngine(ctx).Where(builder.Eq{"lower_name": strings.ToLower(name)})