mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-22 14:29:49 +00:00
core: prevent remote controller from writing files outside files folder (#7352)
This commit is contained in:
+35
-4
@@ -11,22 +11,25 @@ import ChatTests.DBUtils
|
||||
import ChatTests.Utils
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.Except (runExceptT)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.List (find, isPrefixOf)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), versionNumber)
|
||||
import Simplex.Chat.Files (safeFileNameStr)
|
||||
import Simplex.Chat.Library.Commands (parseChatCommand)
|
||||
import qualified Simplex.Chat.Controller as Controller
|
||||
import Simplex.Chat.Mobile.File
|
||||
import Simplex.Chat.Remote (remoteFilesFolder)
|
||||
import Simplex.Chat.Remote (remoteFilesFolder, validRemoteFileName)
|
||||
import Simplex.Chat.Remote.Protocol (remoteStoreFile)
|
||||
import Simplex.Chat.Remote.Types
|
||||
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
|
||||
import Simplex.Messaging.Encoding.String (strEncode)
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.RemoteControl.Types (RCCtrlAddress (..))
|
||||
import System.FilePath ((</>))
|
||||
import System.FilePath (takeFileName, (</>))
|
||||
import Test.Hspec hiding (it)
|
||||
import UnliftIO
|
||||
import UnliftIO.Concurrent
|
||||
@@ -40,10 +43,26 @@ remoteTests = describe "Remote" $ do
|
||||
`shouldSatisfy` \case
|
||||
Right (StartRemoteHost Nothing (Just (RCCtrlAddress _ "Ethernet 2")) (Just 12345)) -> True
|
||||
_ -> False
|
||||
describe "stored file name" $ do
|
||||
it "rejects names with directory components" $ \_ ->
|
||||
filter validRemoteFileName ["../x", "../../etc/passwd", "/etc/cron.d/x", "a/b", "x/", "", ".", ".."]
|
||||
`shouldBe` []
|
||||
it "accepts bare file names" $ \_ ->
|
||||
filter (not . validRemoteFileName) ["test.pdf", "test_1.pdf", ".hidden", "a b.tar.gz"]
|
||||
`shouldBe` []
|
||||
it "sanitizes any name to a real file name" $ \_ ->
|
||||
filter (not . sanitized) fileNames `shouldBe` []
|
||||
it "sanitizes to a name with no directory components" $ \_ ->
|
||||
filter (not . bareName) fileNames `shouldBe` []
|
||||
xdescribe "No compression" $ aroundWith (. ((False, False),)) runRemoteTests
|
||||
xdescribe "Mobile offers compression" $ aroundWith (. ((True, False),)) runRemoteTests
|
||||
xdescribe "Desktop offers compression" $ aroundWith (. ((False, True),)) runRemoteTests
|
||||
describe "With compression" $ aroundWith (. ((True, True),)) runRemoteTests
|
||||
where
|
||||
fileNames :: [FilePath]
|
||||
fileNames = ["", ".", "..", "...", "../x", "../../etc/passwd", "/etc/cron.d/x", "a/b", "x/", "test.pdf", ".hidden", "a b.tar.gz"]
|
||||
sanitized n = let n' = safeFileNameStr n in n' /= "" && n' /= "." && n' /= ".."
|
||||
bareName n = let n' = safeFileNameStr n in n' == takeFileName n'
|
||||
|
||||
runRemoteTests :: SpecWith ((Bool, Bool), TestParams)
|
||||
runRemoteTests = do
|
||||
@@ -243,8 +262,9 @@ remoteStoreFileTest =
|
||||
contactBob desktop bob
|
||||
|
||||
rhs <- readTVarIO (Controller.remoteHostSessions $ chatController desktop)
|
||||
desktopHostStore <- case M.lookup (RHId 1) rhs of
|
||||
Just (_, RHSessionConnected {storePath}) -> pure $ desktopHostFiles </> storePath </> remoteFilesFolder
|
||||
(rhClient, desktopHostStore) <- case M.lookup (RHId 1) rhs of
|
||||
Just (_, RHSessionConnected {rhClient, storePath}) ->
|
||||
pure (rhClient, desktopHostFiles </> storePath </> remoteFilesFolder)
|
||||
_ -> fail "Host session 1 should be started"
|
||||
desktop ##> "/store remote file 1 tests/fixtures/test.pdf"
|
||||
desktop <## "file test.pdf stored on remote host 1"
|
||||
@@ -261,6 +281,17 @@ remoteStoreFileTest =
|
||||
chatReadFile (mobileFiles </> "test_2.pdf") (strEncode key) (strEncode nonce) `shouldReturn` Right (LB.fromStrict src)
|
||||
chatReadFile (desktopHostStore </> "test_2.pdf") (strEncode key) (strEncode nonce) `shouldReturn` Right (LB.fromStrict src)
|
||||
|
||||
-- the host rejects a traversal name before draining the attachment; only calling the protocol
|
||||
-- directly can put such a name on the wire, as /store remote file sanitizes it controller-side
|
||||
runExceptT (remoteStoreFile rhClient "tests/fixtures/test.pdf" "../x") >>= \case
|
||||
Left (RPEInvalidBody _) -> pure ()
|
||||
r -> fail $ "expected RPEInvalidBody, got " <> show r
|
||||
doesFileExist "./tests/tmp/x" `shouldReturn` False
|
||||
-- the undrained attachment did not break the session
|
||||
desktop ##> "/store remote file 1 tests/fixtures/test.pdf"
|
||||
desktop <## "file test_3.pdf stored on remote host 1"
|
||||
B.readFile (mobileFiles </> "test_3.pdf") `shouldReturn` src
|
||||
|
||||
removeFile (desktopHostStore </> "test_1.pdf")
|
||||
removeFile (desktopHostStore </> "test_2.pdf")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user