core: prevent remote controller from writing files outside files folder (#7352)

This commit is contained in:
spaced4ndy
2026-08-07 13:26:48 +00:00
committed by GitHub
parent b832d7c8f7
commit e04e080a3d
4 changed files with 56 additions and 11 deletions
+8 -2
View File
@@ -5,14 +5,20 @@ module Simplex.Chat.Files where
import Simplex.Chat.Controller
import Simplex.Messaging.Util (ifM)
import System.FilePath (combine, splitExtensions)
import System.FilePath (combine, makeValid, splitExtensions, takeFileName)
import UnliftIO.Directory (doesDirectoryExist, doesFileExist, getHomeDirectory, getTemporaryDirectory)
safeFileNameStr :: String -> String
safeFileNameStr = notDots . makeValid . takeFileName
where
notDots n = if n == "." || n == ".." then "_" else n
-- | The file name is sanitized, so the combined path cannot escape the folder.
uniqueCombine :: FilePath -> String -> IO FilePath
uniqueCombine fPath fName = tryCombine (0 :: Int)
where
tryCombine n =
let (name, ext) = splitExtensions fName
let (name, ext) = splitExtensions $ safeFileNameStr fName
suffix = if n == 0 then "" else "_" <> show n
f = fPath `combine` (name <> suffix <> ext)
in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f)
+2 -3
View File
@@ -48,7 +48,7 @@ import Data.Word (Word32)
import Simplex.Chat.Call
import Simplex.Chat.Controller
import Simplex.Chat.Delivery
import Simplex.Chat.Files (getChatTempDirectory)
import Simplex.Chat.Files (getChatTempDirectory, safeFileNameStr)
import Simplex.Chat.Library.Internal
import Simplex.Chat.Web (channelContentChanged, channelProfileUpdated, channelRemoved)
import Simplex.Chat.Messages
@@ -101,7 +101,6 @@ import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (TransportError (..))
import Simplex.Messaging.Util
import Simplex.Messaging.Version
import qualified System.FilePath as FP
import System.Mem.Weak (Weak)
import Text.Read (readMaybe)
import UnliftIO.Concurrent (ThreadId, forkIO, mkWeakThreadId)
@@ -1966,7 +1965,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol})
mkValidFileInvitation :: FileInvitation -> FileInvitation
mkValidFileInvitation fInv@FileInvitation {fileName} = fInv {fileName = FP.makeValid $ FP.takeFileName fileName}
mkValidFileInvitation fInv@FileInvitation {fileName} = fInv {fileName = safeFileNameStr fileName}
validateFileInvitation :: FileInvitation -> CM FileInvitation
validateFileInvitation fInv@FileInvitation {fileName, fileSize}
+11 -2
View File
@@ -65,10 +65,10 @@ import Simplex.Messaging.Util
import Simplex.RemoteControl.Client
import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..), RCVerifiedInvitation (..), verifySignedInvitation)
import Simplex.RemoteControl.Types
import System.FilePath (takeFileName, (</>))
import System.FilePath (takeDirectory, takeFileName, (</>))
import UnliftIO
import UnliftIO.Concurrent (forkIO)
import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, removeDirectoryRecursive, renameFile)
import UnliftIO.Directory (canonicalizePath, copyFile, createDirectoryIfMissing, doesDirectoryExist, removeDirectoryRecursive, renameFile)
remoteFilesFolder :: String
remoteFilesFolder = "simplex_v1_files"
@@ -574,10 +574,19 @@ handleStoreFile rfKN fileName fileSize fileDigest getChunk =
Nothing -> storeFileTo =<< getDefaultFilesFolder
storeFileTo :: FilePath -> CM' (Either RemoteProtocolError FilePath)
storeFileTo dir = liftIO . tryAllErrors' $ do
unless (validRemoteFileName fileName) $ throwError $ RPEInvalidBody "invalid file name"
filePath <- liftIO $ dir `uniqueCombine` fileName
-- resolves symlinks, so it also catches a final component linking outside the folder
canonPath <- liftIO $ canonicalizePath filePath
inDir <- liftIO $ (takeDirectory canonPath ==) <$> canonicalizePath dir
unless inDir $ throwError $ RPEInvalidBody "file path outside of files folder"
receiveEncryptedFile rfKN getChunk fileSize fileDigest filePath
pure filePath
-- The controller only ever sends a bare file name (see storeRemoteFile), so a path is a protocol violation.
validRemoteFileName :: FilePath -> Bool
validRemoteFileName fName = fName == takeFileName fName && fName `notElem` (["", ".", ".."] :: [FilePath])
handleGetFile :: User -> RemoteFile -> Respond -> CM ()
handleGetFile User {userId} RemoteFile {userId = commandUserId, fileId, sent, fileSource = cf'@CryptoFile {filePath}} reply = do
logDebug $ "GetFile: " <> tshow filePath