avoid the dashed ssh hostname class of security holes

Security fix: Disallow hostname starting with a dash, which would get
passed to ssh and be treated an option. This could be used by an attacker
who provides a crafted ssh url (for eg a git remote) to execute arbitrary
code via ssh -oProxyCommand.

No CVE has yet been assigned for this hole.
The same class of security hole recently affected git itself,
CVE-2017-1000117.

Method: Identified all places where ssh is run, by git grep '"ssh"'
Converted them all to use a SshHost, if they did not already, for
specifying the hostname.

SshHost was made a data type with a smart constructor, which rejects
hostnames starting with '-'.

Note that git-annex already contains extensive use of Utility.SafeCommand,
which fixes a similar class of problem where a filename starting with a
dash gets passed to a program which treats it as an option.

This commit was sponsored by Jochen Bartl on Patreon.
This commit is contained in:
Joey Hess 2017-08-17 22:11:31 -04:00
parent 25e55e7c2f
commit df11e54788
No known key found for this signature in database
GPG key ID: DB12DB0FF05F8F38
12 changed files with 106 additions and 61 deletions

View file

@ -39,6 +39,7 @@ import Utility.Tmp
import Utility.FileMode
import Utility.ThreadScheduler
import Utility.Env
import Utility.SshHost
import qualified Data.Text as T
import qualified Data.Map as M
@ -299,12 +300,11 @@ testServer sshinput@(SshInput { inputHostname = Just hn }) = do
if knownhost then "yes" else "no"
, "-n" -- don't read from stdin
, "-p", show (inputPort sshinput)
, genSshHost
(fromJust $ inputHostname sshinput)
(inputUsername sshinput)
, remotecommand
]
parsetranscript . fst <$> sshAuthTranscript sshinput sshopts Nothing
let sshhost = genSshHost
(fromJust $ inputHostname sshinput)
(inputUsername sshinput)
parsetranscript . fst <$> sshAuthTranscript sshinput sshopts sshhost remotecommand Nothing
parsetranscript s =
let cs = map snd $ filter (reported . fst)
[ ("git-annex-shell", GitAnnexShellCapable)
@ -339,9 +339,9 @@ testServer sshinput@(SshInput { inputHostname = Just hn }) = do
{- Runs a ssh command to set up the repository; if it fails shows
- the user the transcript, and if it succeeds, runs an action. -}
sshSetup :: SshInput -> [String] -> Maybe String -> Handler Html -> Handler Html
sshSetup sshinput opts input a = do
(transcript, ok) <- liftAssistant $ sshAuthTranscript sshinput opts input
sshSetup :: SshInput -> [String] -> SshHost -> String -> Maybe String -> Handler Html -> Handler Html
sshSetup sshinput opts sshhost cmd input a = do
(transcript, ok) <- liftAssistant $ sshAuthTranscript sshinput opts sshhost cmd input
if ok
then do
liftAssistant $ expireCachedCred $ getLogin sshinput
@ -367,8 +367,8 @@ sshErr sshinput msg
- cached password. ssh is coaxed to use git-annex as SSH_ASKPASS
- to get the password.
-}
sshAuthTranscript :: SshInput -> [String] -> (Maybe String) -> Assistant (String, Bool)
sshAuthTranscript sshinput opts input = case inputAuthMethod sshinput of
sshAuthTranscript :: SshInput -> [String] -> SshHost -> String -> (Maybe String) -> Assistant (String, Bool)
sshAuthTranscript sshinput opts sshhost cmd input = case inputAuthMethod sshinput of
ExistingSshKey -> liftIO $ go [passwordprompts 0] Nothing
CachedPassword -> setupAskPass
Password -> do
@ -379,7 +379,7 @@ sshAuthTranscript sshinput opts input = case inputAuthMethod sshinput of
geti f = maybe "" T.unpack (f sshinput)
go extraopts environ = processTranscript'
(askPass environ (proc "ssh" (extraopts ++ opts)))
(askPass environ (proc "ssh" (extraopts ++ opts ++ [fromSshHost sshhost, cmd])))
-- Always provide stdin, even when empty.
(Just (fromMaybe "" input))
@ -521,10 +521,11 @@ prepSsh' needsinit origsshdata sshdata keypair a
]
a sshdata
| otherwise = sshSetup (mkSshInput origsshdata)
[ "-p", show (sshPort origsshdata)
, genSshHost (sshHostName origsshdata) (sshUserName origsshdata)
, remoteCommand
] Nothing (a sshdata)
[ "-p", show (sshPort origsshdata)
]
(genSshHost (sshHostName origsshdata) (sshUserName origsshdata))
remoteCommand
Nothing (a sshdata)
where
remotedir = T.unpack $ sshDirectory sshdata
remoteCommand = shellWrap $ intercalate "&&" $ catMaybes
@ -625,7 +626,7 @@ getMakeRsyncNetGCryptR :: SshData -> RepoKey -> Handler Html
getMakeRsyncNetGCryptR sshdata NoRepoKey = whenGcryptInstalled $
withNewSecretKey $ getMakeRsyncNetGCryptR sshdata . RepoKey
getMakeRsyncNetGCryptR sshdata (RepoKey keyid) = whenGcryptInstalled $
sshSetup (mkSshInput sshdata) [sshhost, gitinit] Nothing $
sshSetup (mkSshInput sshdata) [] sshhost gitinit Nothing $
makeGCryptRepo NewRepo keyid sshdata
where
sshhost = genSshHost (sshHostName sshdata) (sshUserName sshdata)
@ -661,11 +662,9 @@ prepRsyncNet sshinput reponame a = do
, sshCapabilities = [RsyncCapable]
}
let sshhost = genSshHost (sshHostName sshdata) (sshUserName sshdata)
let torsyncnet cmd = filter (not . null)
[ if knownhost then "" else sshOpt "StrictHostKeyChecking" "no"
, sshhost
, cmd
]
let torsyncnet
| knownhost = []
| otherwise = [sshOpt "StrictHostKeyChecking" "no"]
{- I'd prefer to separate commands with && , but
- rsync.net's shell does not support that. -}
let remotecommand = intercalate ";"
@ -674,7 +673,8 @@ prepRsyncNet sshinput reponame a = do
, "dd of=.ssh/authorized_keys oflag=append conv=notrunc"
, "mkdir -p " ++ T.unpack (sshDirectory sshdata)
]
sshSetup sshinput (torsyncnet remotecommand) (Just $ sshPubKey keypair) (a sshdata)
sshSetup sshinput torsyncnet sshhost remotecommand
(Just $ sshPubKey keypair) (a sshdata)
isRsyncNet :: Maybe Text -> Bool
isRsyncNet Nothing = False