owpengram-server/internal/rpc/request_preflight.go

137 lines
5.3 KiB
Go

package rpc
import (
"encoding/binary"
"fmt"
"github.com/gotd/td/bin"
"github.com/gotd/td/tg"
appfiles "telesrv/internal/app/files"
"telesrv/internal/domain"
)
const tlVectorTypeID = uint32(0x1cb5c415)
type requestVectorPolicy struct {
vectorOffset int
max int
minElemBytes int
tooLong func() error
}
// requestVectorPolicies mirrors limits already enforced by typed handlers, but does so before
// gotd's generated decoder materializes attacker-controlled interface slices. users.getUsers is
// the one newly introduced cap: TDesktop's four current call sites all send exactly one user.
var requestVectorPolicies = map[uint32]requestVectorPolicy{
tg.UsersGetUsersRequestTypeID: {vectorOffset: 4, max: 100, minElemBytes: 4, tooLong: inputRequestTooLongErr},
tg.UsersGetRequirementsToContactRequestTypeID: {vectorOffset: 4, max: maxRequirementsToContactUsers, minElemBytes: 4, tooLong: limitInvalidErr},
tg.ContactsImportContactsRequestTypeID: {vectorOffset: 4, max: maxContactImportBatch, minElemBytes: 4, tooLong: limitInvalidErr},
tg.ContactsDeleteContactsRequestTypeID: {vectorOffset: 4, max: maxContactDeleteBatch, minElemBytes: 4, tooLong: limitInvalidErr},
tg.ContactsEditCloseFriendsRequestTypeID: {vectorOffset: 4, max: maxCloseFriendsCount, minElemBytes: 8, tooLong: limitInvalidErr},
tg.ContactsSetBlockedRequestTypeID: {vectorOffset: 8, max: maxContactSetBlocked, minElemBytes: 4, tooLong: limitInvalidErr},
tg.MessagesGetMessagesRequestTypeID: {vectorOffset: 4, max: maxGetMessagesIDs, minElemBytes: 4, tooLong: limitInvalidErr},
tg.MessagesGetChatsRequestTypeID: {vectorOffset: 4, max: maxGetMessagesIDs, minElemBytes: 8, tooLong: limitInvalidErr},
tg.MessagesGetPeerDialogsRequestTypeID: {vectorOffset: 4, max: maxDialogInputPeers, minElemBytes: 4, tooLong: limitInvalidErr},
tg.MessagesReadMessageContentsRequestTypeID: {vectorOffset: 4, max: maxGetMessagesIDs, minElemBytes: 4, tooLong: limitInvalidErr},
tg.MessagesGetCustomEmojiDocumentsRequestTypeID: {vectorOffset: 4, max: maxEmojiDocuments, minElemBytes: 8, tooLong: limitInvalidErr},
tg.MessagesDeleteMessagesRequestTypeID: {vectorOffset: 8, max: domain.MaxDeleteMessageIDs, minElemBytes: 4, tooLong: limitInvalidErr},
tg.MessagesCreateChatRequestTypeID: {vectorOffset: 8, max: 200, minElemBytes: 4, tooLong: limitInvalidErr},
tg.ChannelsGetChannelsRequestTypeID: {vectorOffset: 4, max: maxGetMessagesIDs, minElemBytes: 4, tooLong: limitInvalidErr},
}
func preflightRPCRequest(id uint32, b *bin.Buffer) error {
if b == nil {
return inputRequestInvalidErr()
}
if policy, ok := requestVectorPolicies[id]; ok {
if err := preflightFixedVector(b.Buf, policy); err != nil {
return err
}
}
switch id {
case tg.UploadSaveFilePartRequestTypeID:
return preflightUploadPart(b.Buf, 16, false)
case tg.UploadSaveBigFilePartRequestTypeID:
return preflightUploadPart(b.Buf, 20, true)
default:
return nil
}
}
func preflightFixedVector(raw []byte, policy requestVectorPolicy) error {
if policy.vectorOffset < 4 || policy.minElemBytes <= 0 || len(raw) < policy.vectorOffset+8 {
return inputRequestInvalidErr()
}
if binary.LittleEndian.Uint32(raw[policy.vectorOffset:]) != tlVectorTypeID {
return inputRequestInvalidErr()
}
count := int64(int32(binary.LittleEndian.Uint32(raw[policy.vectorOffset+4:])))
if count < 0 {
return inputRequestInvalidErr()
}
remaining := int64(len(raw) - policy.vectorOffset - 8)
// Check the cheapest possible encoding before the policy cap. A forged MaxInt32 count with
// a truncated body is malformed, not merely a large valid request, and is rejected O(1).
if count > remaining/int64(policy.minElemBytes) {
return inputRequestInvalidErr()
}
if count > int64(policy.max) {
if policy.tooLong != nil {
return policy.tooLong()
}
return inputRequestTooLongErr()
}
return nil
}
func preflightUploadPart(raw []byte, bytesOffset int, big bool) error {
if big {
if len(raw) < 20 {
return inputRequestInvalidErr()
}
totalParts := int32(binary.LittleEndian.Uint32(raw[16:20]))
if totalParts <= 0 || totalParts > appfiles.MaxUploadParts {
return filePartInvalidErr()
}
}
n, encoded, err := tlBytesSizeAt(raw, bytesOffset)
if err != nil {
return inputRequestInvalidErr()
}
if encoded != len(raw)-bytesOffset {
return inputRequestInvalidErr()
}
if n > appfiles.MaxUploadPartBytes {
return filePartTooBigErr()
}
return nil
}
// tlBytesSizeAt parses a TL bytes prefix without copying the payload. encoded includes prefix,
// payload and 4-byte padding.
func tlBytesSizeAt(raw []byte, offset int) (n, encoded int, err error) {
if offset < 0 || offset >= len(raw) {
return 0, 0, fmt.Errorf("bytes prefix out of range")
}
first := raw[offset]
prefix := 1
switch {
case first < 254:
n = int(first)
case first == 254:
if len(raw)-offset < 4 {
return 0, 0, fmt.Errorf("truncated long bytes prefix")
}
n = int(raw[offset+1]) | int(raw[offset+2])<<8 | int(raw[offset+3])<<16
prefix = 4
default:
return 0, 0, fmt.Errorf("invalid bytes prefix")
}
total := prefix + n
padding := (4 - total%4) % 4
if total > len(raw)-offset-padding {
return 0, 0, fmt.Errorf("truncated bytes payload")
}
return n, total + padding, nil
}