core: validate full encoded profile size

Add checkProfileSize / checkGroupProfileSize that encode the full
ChatMessage and check against maxEncodedInfoLength, so a long
displayName/bio combined with a near-max image is also caught at
command time instead of failing later at send time with CEException.

Run alongside the existing checkProfileImageSize (image-only cap of
12500 bytes, matching mobile UIs) in CreateActiveUser, updateProfile_,
newGroup, runUpdateGroupProfile. Update genProfileImg to fit the cap.
This commit is contained in:
shum
2026-05-06 15:19:20 +00:00
parent 456051a2ec
commit 00164f07c9
2 changed files with 25 additions and 2 deletions
+21 -1
View File
@@ -158,6 +158,22 @@ checkProfileImageSize = mapM_ $ \(ImageData t) ->
let size = T.length t
in when (size > maxProfileImageSize) $ throwCmdError $ "Profile image is too large " <> show size
checkProfileSize :: Profile -> CM ()
checkProfileSize profile = do
vr <- chatVersionRange
let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent = XInfo profile}
case encodeChatMessage maxEncodedInfoLength info of
ECMEncoded _ -> pure ()
ECMLarge -> throwCmdError "Profile is too large"
checkGroupProfileSize :: GroupProfile -> CM ()
checkGroupProfileSize gProfile = do
vr <- chatVersionRange
let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent = XGrpInfo gProfile}
case encodeChatMessage maxEncodedInfoLength info of
ECMEncoded _ -> pure ()
ECMLarge -> throwCmdError "Group profile is too large"
imageExtensions :: [String]
imageExtensions = [".jpg", ".jpeg", ".png", ".gif"]
@@ -358,9 +374,10 @@ processChatCommand :: VersionRangeChat -> NetworkRequestMode -> ChatCommand -> C
processChatCommand vr nm = \case
ShowActiveUser -> withUser' $ pure . CRActiveUser
CreateActiveUser NewUser {profile, pastTimestamp, userChatRelay} -> do
forM_ profile $ \Profile {displayName, image} -> do
forM_ profile $ \p@Profile {displayName, image} -> do
checkValidName displayName
checkProfileImageSize image
checkProfileSize p
p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile
u <- asks currentUser
users <- withFastStore' getUsers
@@ -3640,6 +3657,7 @@ processChatCommand vr nm = \case
| otherwise = do
when (n /= n') $ checkValidName n'
checkProfileImageSize img'
checkProfileSize p'
-- read contacts before user update to correctly merge preferences
contacts <- withFastStore' $ \db -> getUserContacts db vr user
user' <- updateUser
@@ -3725,6 +3743,7 @@ processChatCommand vr nm = \case
assertUserGroupRole gInfo GROwner
when (n /= n') $ checkValidName n'
checkProfileImageSize img'
checkGroupProfileSize p'
gInfo' <- withStore $ \db -> updateGroupProfile db user gInfo p'
msg <- case businessChat of
Just BusinessChatInfo {businessId} -> do
@@ -3868,6 +3887,7 @@ processChatCommand vr nm = \case
newGroup user incognito gProfile@GroupProfile {displayName, image} useRelays memberId groupKeys_ publicMemberCount_ = do
checkValidName displayName
checkProfileImageSize image
checkGroupProfileSize gProfile
-- [incognito] generate incognito profile for group membership
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
withFastStore $ \db -> createNewGroup db vr user gProfile incognitoProfile useRelays memberId groupKeys_ publicMemberCount_
+4 -1
View File
@@ -24,6 +24,7 @@ import Data.Maybe (fromMaybe)
import Data.String
import qualified Data.Text as T
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..))
import Simplex.Chat.Library.Commands (maxProfileImageSize)
import Simplex.Chat.Markdown (viewName)
import Simplex.Chat.Messages.CIContent (e2eInfoNoPQText, e2eInfoPQText)
import Simplex.Chat.Protocol
@@ -235,7 +236,9 @@ genProfileImg = do
g <- C.newRandom
atomically $ B64.encode <$> C.randomBytes lrgLen g
where
lrgLen = maxEncodedInfoLength * 3 `div` 4 - 420
-- raw bytes that base64-encode to fit maxProfileImageSize when prefixed with "data:image/png;base64,"
lrgLen = (maxProfileImageSize - imagePrefixLen) * 3 `div` 4 - 1
imagePrefixLen = 22
-- PQ combinators /