3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-13 07:29:30 +01:00

improve security properties of INVITE

See #1171.
This commit is contained in:
Shivaram Lingamneni 2020-10-25 20:40:41 -04:00
parent 9670d96282
commit 42d246b557
5 changed files with 57 additions and 18 deletions

View File

@ -556,6 +556,10 @@ channels:
# than this value will get an empty response to /LIST (a time period of 0 disables) # than this value will get an empty response to /LIST (a time period of 0 disables)
list-delay: 0s list-delay: 0s
# INVITE to an invite-only channel expires after this amount of time
# (0 or omit for no expiration):
invite-expiration: 24h
# operator classes # operator classes
oper-classes: oper-classes:
# local operator # local operator

View File

@ -677,6 +677,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
chname := channel.name chname := channel.name
chcfname := channel.nameCasefolded chcfname := channel.nameCasefolded
founder := channel.registeredFounder founder := channel.registeredFounder
createdAt := channel.createdTime
chkey := channel.key chkey := channel.key
limit := channel.userLimit limit := channel.userLimit
chcount := len(channel.members) chcount := len(channel.members)
@ -695,7 +696,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
// 3. people invited with INVITE can join // 3. people invited with INVITE can join
hasPrivs := isSajoin || (founder != "" && founder == details.account) || hasPrivs := isSajoin || (founder != "" && founder == details.account) ||
(persistentMode != 0 && persistentMode != modes.Voice) || (persistentMode != 0 && persistentMode != modes.Voice) ||
client.CheckInvited(chcfname) client.CheckInvited(chcfname, createdAt)
if !hasPrivs { if !hasPrivs {
if limit != 0 && chcount >= limit { if limit != 0 && chcount >= limit {
return errLimitExceeded return errLimitExceeded
@ -1475,23 +1476,33 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
// Invite invites the given client to the channel, if the inviter can do so. // Invite invites the given client to the channel, if the inviter can do so.
func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuffer) { func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuffer) {
chname := channel.Name() channel.stateMutex.RLock()
if channel.flags.HasMode(modes.InviteOnly) && !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) { chname := channel.name
rb.Add(nil, inviter.server.name, ERR_CHANOPRIVSNEEDED, inviter.Nick(), chname, inviter.t("You're not a channel operator")) chcfname := channel.nameCasefolded
return createdAt := channel.createdTime
} _, inviterPresent := channel.members[inviter]
_, inviteePresent := channel.members[invitee]
channel.stateMutex.RUnlock()
if !channel.hasClient(inviter) { if !inviterPresent {
rb.Add(nil, inviter.server.name, ERR_NOTONCHANNEL, inviter.Nick(), chname, inviter.t("You're not on that channel")) rb.Add(nil, inviter.server.name, ERR_NOTONCHANNEL, inviter.Nick(), chname, inviter.t("You're not on that channel"))
return return
} }
if channel.hasClient(invitee) { inviteOnly := channel.flags.HasMode(modes.InviteOnly)
if inviteOnly && !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) {
rb.Add(nil, inviter.server.name, ERR_CHANOPRIVSNEEDED, inviter.Nick(), chname, inviter.t("You're not a channel operator"))
return
}
if inviteePresent {
rb.Add(nil, inviter.server.name, ERR_USERONCHANNEL, inviter.Nick(), invitee.Nick(), chname, inviter.t("User is already on that channel")) rb.Add(nil, inviter.server.name, ERR_USERONCHANNEL, inviter.Nick(), invitee.Nick(), chname, inviter.t("User is already on that channel"))
return return
} }
invitee.Invite(channel.NameCasefolded()) if inviteOnly {
invitee.Invite(chcfname, createdAt)
}
for _, member := range channel.Members() { for _, member := range channel.Members() {
if member == inviter || member == invitee || !channel.ClientIsAtLeast(member, modes.Halfop) { if member == inviter || member == invitee || !channel.ClientIsAtLeast(member, modes.Halfop) {

View File

@ -84,7 +84,7 @@ type Client struct {
destroyed bool destroyed bool
modes modes.ModeSet modes modes.ModeSet
hostname string hostname string
invitedTo utils.StringSet invitedTo map[string]channelInvite
isSTSOnly bool isSTSOnly bool
languages []string languages []string
lastActive time.Time // last time they sent a command that wasn't PONG or similar lastActive time.Time // last time they sent a command that wasn't PONG or similar
@ -1764,26 +1764,45 @@ func (client *Client) removeChannel(channel *Channel) {
} }
} }
type channelInvite struct {
channelCreatedAt time.Time
invitedAt time.Time
}
// Records that the client has been invited to join an invite-only channel // Records that the client has been invited to join an invite-only channel
func (client *Client) Invite(casefoldedChannel string) { func (client *Client) Invite(casefoldedChannel string, channelCreatedAt time.Time) {
now := time.Now().UTC()
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
if client.invitedTo == nil { if client.invitedTo == nil {
client.invitedTo = make(utils.StringSet) client.invitedTo = make(map[string]channelInvite)
} }
client.invitedTo.Add(casefoldedChannel) client.invitedTo[casefoldedChannel] = channelInvite{
channelCreatedAt: channelCreatedAt,
invitedAt: now,
}
return
} }
// Checks that the client was invited to join a given channel // Checks that the client was invited to join a given channel
func (client *Client) CheckInvited(casefoldedChannel string) (invited bool) { func (client *Client) CheckInvited(casefoldedChannel string, createdTime time.Time) (invited bool) {
config := client.server.Config()
expTime := time.Duration(config.Channels.InviteExpiration)
now := time.Now().UTC()
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
invited = client.invitedTo.Has(casefoldedChannel) curInvite, ok := client.invitedTo[casefoldedChannel]
// joining an invited channel "uses up" your invite, so you can't rejoin on kick if ok {
delete(client.invitedTo, casefoldedChannel) // joining an invited channel "uses up" your invite, so you can't rejoin on kick
delete(client.invitedTo, casefoldedChannel)
}
invited = ok && (expTime == time.Duration(0) || now.Sub(curInvite.invitedAt) < expTime) &&
createdTime.Equal(curInvite.channelCreatedAt)
return return
} }

View File

@ -574,7 +574,8 @@ type Config struct {
OperatorOnly bool `yaml:"operator-only"` OperatorOnly bool `yaml:"operator-only"`
MaxChannelsPerAccount int `yaml:"max-channels-per-account"` MaxChannelsPerAccount int `yaml:"max-channels-per-account"`
} }
ListDelay time.Duration `yaml:"list-delay"` ListDelay time.Duration `yaml:"list-delay"`
InviteExpiration custime.Duration `yaml:"invite-expiration"`
} }
OperClasses map[string]*OperClassConfig `yaml:"oper-classes"` OperClasses map[string]*OperClassConfig `yaml:"oper-classes"`

View File

@ -528,6 +528,10 @@ channels:
# than this value will get an empty response to /LIST (a time period of 0 disables) # than this value will get an empty response to /LIST (a time period of 0 disables)
list-delay: 0s list-delay: 0s
# INVITE to an invite-only channel expires after this amount of time
# (0 or omit for no expiration):
invite-expiration: 24h
# operator classes # operator classes
oper-classes: oper-classes:
# local operator # local operator