Version in base suite: 10.20250416-2 Base version: git-annex_10.20250416-2 Target version: git-annex_10.20250416-2+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/g/git-annex/git-annex_10.20250416-2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/g/git-annex/git-annex_10.20250416-2+deb13u1.dsc changelog | 7 patches/debian-changes | 1928 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1935 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp4h_giczu/git-annex_10.20250416-2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp4h_giczu/git-annex_10.20250416-2+deb13u1.dsc: no acceptable signature found diff -Nru git-annex-10.20250416/debian/changelog git-annex-10.20250416/debian/changelog --- git-annex-10.20250416/debian/changelog 2025-07-01 10:23:57.000000000 +0000 +++ git-annex-10.20250416/debian/changelog 2026-10-31 10:55:29.000000000 +0000 @@ -1,3 +1,10 @@ +git-annex (10.20250416-2+deb13u1) trixie-security; urgency=high + + * Address CVE-2026-112818. + Thanks to Joey Hess for preparing the backport of the fix. + + -- Sean Whitton Sat, 31 Oct 2026 10:55:29 +0000 + git-annex (10.20250416-2) unstable; urgency=medium * Cherry pick compatibility fix for Git 2.5.0 (Closes: #1108526). diff -Nru git-annex-10.20250416/debian/patches/debian-changes git-annex-10.20250416/debian/patches/debian-changes --- git-annex-10.20250416/debian/patches/debian-changes 2025-07-01 10:23:57.000000000 +0000 +++ git-annex-10.20250416/debian/patches/debian-changes 2026-10-31 10:55:29.000000000 +0000 @@ -15,6 +15,274 @@ clone`.) A single combined diff, containing all the changes, follows. +--- git-annex-10.20250416.orig/Annex.hs ++++ git-annex-10.20250416/Annex.hs +@@ -204,6 +204,7 @@ data AnnexState = AnnexState + , preferredcontentmap :: Maybe (FileMatcherMap Annex) + , requiredcontentmap :: Maybe (FileMatcherMap Annex) + , remoteconfigmap :: Maybe (M.Map UUID RemoteConfig) ++ , verifiedremoteconfigmap :: Maybe (Maybe (M.Map UUID RemoteConfig)) + , clusters :: Maybe (Annex Clusters) + , maxsizes :: Maybe (M.Map UUID MaxSize) + , forcetrust :: TrustMap +@@ -260,6 +261,7 @@ newAnnexState c r = do + , preferredcontentmap = Nothing + , requiredcontentmap = Nothing + , remoteconfigmap = Nothing ++ , verifiedremoteconfigmap = Nothing + , clusters = Nothing + , maxsizes = Nothing + , forcetrust = M.empty +--- git-annex-10.20250416.orig/Annex/Hook.hs ++++ git-annex-10.20250416/Annex/Hook.hs +@@ -26,14 +26,27 @@ preCommitHook = Git.Hook (literalOsPath + + postReceiveHook :: Git.Hook + postReceiveHook = Git.Hook (literalOsPath "post-receive") +- -- Only run git-annex post-receive when git-annex supports it, +- -- to avoid failing if the repository with this hook is used +- -- with an older version of git-annex. +- (mkHookScript "if git annex post-receive --help >/dev/null 2>&1; then git annex post-receive; fi") ++ (mkHookScript (runHookCommandWhenSupported "post-receive")) + -- This is an old version of the hook script. + [ mkHookScript "git annex post-receive" + ] + ++-- Only run a git-annex hook command when git-annex supports it, to avoid ++-- failing if the repository with a hook is used with an older version of ++-- git-annex. ++runHookCommandWhenSupported :: String -> String ++runHookCommandWhenSupported command = concat ++ [ "if git annex " ++ , command ++ , " --help >/dev/null 2>&1; then git annex " ++ , command ++ , "; fi" ++ ] ++ ++prePushHook :: Git.Hook ++prePushHook = Git.Hook (literalOsPath "pre-push") ++ (mkHookScript (runHookCommandWhenSupported "pre-push")) [] ++ + postCheckoutHook :: Git.Hook + postCheckoutHook = Git.Hook (literalOsPath "post-checkout") smudgeHook [] + +@@ -80,16 +93,21 @@ hookWrite :: Git.Hook -> Annex () + hookWrite h = unlessM (inRepo $ Git.hookWrite h) $ + hookWarning h "already exists, not configuring" + ++tryHookWrite :: Git.Hook -> Annex (Either SomeException Bool) ++tryHookWrite h = tryNonAsync (inRepo $ Git.hookWrite h) ++ + hookUnWrite :: Git.Hook -> Annex () + hookUnWrite h = unlessM (inRepo $ Git.hookUnWrite h) $ + hookWarning h "contents modified; not deleting. Edit it to remove call to git annex." + + hookWarning :: Git.Hook -> String -> Annex () +-hookWarning h msg = do ++hookWarning h msg = warning . UnquotedString =<< hookMessage h msg ++ ++hookMessage :: Git.Hook -> String -> Annex String ++hookMessage h msg = do + r <- gitRepo +- warning $ UnquotedString $ +- fromOsPath (Git.hookName h) ++ +- " hook (" ++ fromOsPath (Git.hookFile h r) ++ ") " ++ msg ++ return $ fromOsPath (Git.hookName h) ++ ++ " hook (" ++ fromOsPath (Git.hookFile h r) ++ ") " ++ msg + + {- To avoid checking if the hook exists every time, the existing hooks + - are cached. -} +@@ -159,3 +177,11 @@ outputOfAnnexHook hook commandcfg = ifM + Nothing -> return Nothing + Just command -> liftIO $ + Just <$> readProcess "sh" ["-c", command] ++ ++enablePrePushHook :: Annex (Maybe String) ++enablePrePushHook = tryHookWrite prePushHook >>= \case ++ Right True -> return Nothing ++ Right False -> Just <$> hookMessage prePushHook ++ "is not configured by git-annex." ++ Left _ex -> Just <$> hookMessage prePushHook ++ "does not exist and unable to write it." +--- git-annex-10.20250416.orig/Annex/Init.hs ++++ git-annex-10.20250416/Annex/Init.hs +@@ -158,6 +158,7 @@ initialize' startupannex mversion _inita + unlessM isBareRepo $ do + hookWrite postCheckoutHook + hookWrite postMergeHook ++ hookWrite prePushHook + + AdjustedBranch.checkAdjustedClone >>= \case + AdjustedBranch.InAdjustedClone -> return () +@@ -184,6 +185,7 @@ uninitialize = do + hookUnWrite postReceiveHook + hookUnWrite postCheckoutHook + hookUnWrite postMergeHook ++ hookUnWrite prePushHook + deconfigureSmudgeFilter + removeRepoUUID + removeVersion +@@ -473,7 +475,7 @@ autoEnableSpecialRemotes remotelist = do + _ <- remotelist + rp <- fromRepo Git.repoPath + withNullHandle $ \nullh -> gitAnnexChildProcess "init" +- [ Param "--autoenable" ] ++ [ Param "--autoenable", Param "--force" ] + (\p -> p + { std_out = UseHandle nullh + , std_err = UseHandle nullh +--- git-annex-10.20250416.orig/Annex/Locations.hs ++++ git-annex-10.20250416/Annex/Locations.hs +@@ -57,6 +57,10 @@ module Annex.Locations ( + gitAnnexRestageLog, + gitAnnexRestageLogOld, + gitAnnexRestageLock, ++ gitAnnexVerifiedRemoteLog, ++ gitAnnexVerifiedRemoteLock, ++ gitAnnexVerifyingRemoteLog, ++ gitAnnexVerifyingRemoteLock, + gitAnnexAdjustedBranchUpdateLog, + gitAnnexAdjustedBranchUpdateLock, + gitAnnexMigrateLog, +@@ -438,6 +442,23 @@ gitAnnexRestageLogOld r = gitAnnexDir r + gitAnnexRestageLock :: Git.Repo -> OsPath + gitAnnexRestageLock r = gitAnnexDir r literalOsPath "restage.lck" + ++{- .git/annex/verified-remote.log is used to store the verified configs ++ - of remotes. The git-annex branch remote.log may have newer versions, ++ - which will not be used until after they have been verified. -} ++gitAnnexVerifiedRemoteLog :: Git.Repo -> OsPath ++gitAnnexVerifiedRemoteLog r = gitAnnexDir r literalOsPath "verified-remote.log" ++ ++gitAnnexVerifiedRemoteLock :: Git.Repo -> OsPath ++gitAnnexVerifiedRemoteLock r = gitAnnexDir r literalOsPath "verified-remote.lck" ++ ++{- git/annex/verifying-remote.log is used by git-annex verify to record ++ - the config that a user is in the process of verifying. -} ++gitAnnexVerifyingRemoteLog :: Git.Repo -> OsPath ++gitAnnexVerifyingRemoteLog r = gitAnnexDir r literalOsPath "verifying-remote.log" ++ ++gitAnnexVerifyingRemoteLock :: Git.Repo -> OsPath ++gitAnnexVerifyingRemoteLock r = gitAnnexDir r literalOsPath "verifying-remote.lck" ++ + {- .git/annex/adjust.log is used to log when the adjusted branch needs to + - be updated. -} + gitAnnexAdjustedBranchUpdateLog :: Git.Repo -> OsPath +--- git-annex-10.20250416.orig/Annex/SpecialRemote.hs ++++ git-annex-10.20250416/Annex/SpecialRemote.hs +@@ -14,6 +14,7 @@ module Annex.SpecialRemote ( + + import Annex.Common + import Annex.SpecialRemote.Config ++import Annex.Hook + import Types.Remote (RemoteConfig, SetupStage(..), typename, setup) + import Types.GitConfig + import Types.ProposedAccepted +@@ -102,13 +103,19 @@ autoEnable = do + -- remote, and an attacker could leverage + -- autoenabling it as part of an attack. + (Just name, Right t) | safeOutput name == name -> do +- showSideAction $ UnquotedString $ "Auto enabling special remote " ++ name +- dummycfg <- liftIO dummyRemoteGitConfig +- tryNonAsync (setup t (AutoEnable c) (Just u) Nothing c dummycfg) >>= \case +- Left e -> warning (UnquotedString (show e)) +- Right (_c, _u) -> +- when (cu /= u) $ +- setConfig (remoteAnnexConfig c "config-uuid") (fromUUID cu) ++ enablePrePushHook >>= \case ++ Nothing -> do ++ showSideAction $ UnquotedString $ "Auto enabling special remote " ++ name ++ dummycfg <- liftIO dummyRemoteGitConfig ++ tryNonAsync (setup t (AutoEnable c) (Just u) Nothing c dummycfg) >>= \case ++ Left e -> warning (UnquotedString (show e)) ++ Right (_c, _u) -> do ++ setRemoteConfigVerification name Remote.RemoteConfigUnverifiedAutoEnabled ++ when (cu /= u) $ ++ setConfig (remoteAnnexConfig c "config-uuid") (fromUUID cu) ++ Just msg -> warning $ UnquotedString $ ++ "Cannot auto enable special remote " ++ ++ name ++ " because " ++ msg + _ -> return () + where + getenabledremotes = M.fromList +--- git-annex-10.20250416.orig/Annex/SpecialRemote/Config.hs ++++ git-annex-10.20250416/Annex/SpecialRemote/Config.hs +@@ -112,6 +112,23 @@ annexObjectsField = Accepted "annexobjec + annexObjects :: ParsedRemoteConfig -> Bool + annexObjects = fromMaybe False . getRemoteConfigValue annexObjectsField + ++{- This is the order used in examples. Putting the typeField first ++ - in particular makes it easier to understand a RemoteConfig. -} ++conventionalConfigOrder :: RemoteConfigField -> RemoteConfigField -> Ordering ++conventionalConfigOrder a b = compare (n a) (n b) ++ where ++ n :: RemoteConfigField -> (Int, RemoteConfigField) ++ n f ++ | f == typeField = (1, f) ++ | f == exportTreeField = (3, f) ++ | f == importTreeField = (4, f) ++ -- 5 ++ | f == autoEnableField = (6, f) ++ | f == chunkField = (7, f) ++ | f == chunksizeField = (8, f) ++ | f == encryptionField = (9, f) ++ | otherwise = (5, f) ++ + {- Parsers for fields that are common to all special remotes. -} + commonFieldParsers :: [RemoteConfigFieldParser] + commonFieldParsers = +@@ -165,6 +182,16 @@ sameasInherits = S.fromList + , chunkField + ] + ++{- Fields that are used for encrypted special remotes, and are not user ++ - visible. -} ++hiddenEncryptionFields :: S.Set RemoteConfigField ++hiddenEncryptionFields = S.fromList ++ [ macField ++ , cipherField ++ , cipherkeysField ++ , pubkeysField ++ ] ++ + {- Each RemoteConfig that has a sameas-uuid inherits some fields + - from it. Such fields can only be set by inheritance; the RemoteConfig + - cannot provide values from them. -} +--- git-annex-10.20250416.orig/CHANGELOG ++++ git-annex-10.20250416/CHANGELOG +@@ -1,3 +1,31 @@ ++git-annex (10.20250416.1) upstream; urgency=high ++ ++ * This releases fixes a security hole that allowed private data ++ exfiltration from special remotes. ++ * verifyremote: New command, used to verify the configuration of special ++ remotes is safe. ++ * enableremote: Display configuration and prompt for verifyremote to be ++ run. Until it is verified, the special remote cannot be used, ++ and git pushes to all remotes will be prevented. ++ * When a special remote has been autoenabled, allow it to be used without ++ verification, but prevent sending annexed files or git pushes to any ++ remote until it is verified. ++ * After upgrading git-annex, existing special remotes will continue to work ++ as before. If a special remote could have been configured by a third party, ++ you should verify its configuration using the `git-annex verifyremote` ++ command. ++ * Changes to special remote configuration received in the git-annex ++ branch no longer take effect until git-annex verifyremote is run to review ++ and verify the new configuration. ++ * init: When run in an already initialized repository, avoid ++ autoenabling new special remotes, since files received from ++ them could leak out via other git-annex processes running in the ++ repository. ++ * init --autoenable: Refuse to perform probably unsafe action unless forced. ++ * Added remote.name.annex-verified git config. ++ ++ -- Joey Hess Fri, 16 Oct 2026 09:29:27 -0400 ++ + git-annex (10.20250416) upstream; urgency=medium + + * Added the mask special remote. --- git-annex-10.20250416.orig/COPYRIGHT +++ git-annex-10.20250416/COPYRIGHT @@ -63,11 +63,14 @@ License: GPL-2 @@ -54,6 +322,577 @@ License: BSD-2-clause Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions +--- git-annex-10.20250416.orig/CmdLine/GitAnnex.hs ++++ git-annex-10.20250416/CmdLine/GitAnnex.hs +@@ -57,6 +57,7 @@ import qualified Command.Init + import qualified Command.Describe + import qualified Command.InitRemote + import qualified Command.EnableRemote ++import qualified Command.VerifyRemote + import qualified Command.ConfigRemote + import qualified Command.RenameRemote + import qualified Command.EnableTor +@@ -69,6 +70,7 @@ import qualified Command.AddUnused + import qualified Command.Unlock + import qualified Command.Lock + import qualified Command.PreCommit ++import qualified Command.PrePush + import qualified Command.PostReceive + import qualified Command.FilterBranch + import qualified Command.Find +@@ -177,6 +179,7 @@ cmds testoptparser testrunner mkbenchmar + , Command.Describe.cmd + , Command.InitRemote.cmd + , Command.EnableRemote.cmd ++ , Command.VerifyRemote.cmd + , Command.ConfigRemote.cmd + , Command.RenameRemote.cmd + , Command.EnableTor.cmd +@@ -186,6 +189,7 @@ cmds testoptparser testrunner mkbenchmar + , Command.Uninit.cmd + , Command.Reinit.cmd + , Command.PreCommit.cmd ++ , Command.PrePush.cmd + , Command.PostReceive.cmd + , Command.NumCopies.cmd + , Command.MinCopies.cmd +--- git-annex-10.20250416.orig/CmdLine/GitRemoteAnnex.hs ++++ git-annex-10.20250416/CmdLine/GitRemoteAnnex.hs +@@ -27,6 +27,7 @@ import qualified Annex.BranchState + import qualified Annex.Url as Url + import qualified Types.Remote as Remote + import qualified Logs.Remote ++import qualified Logs.VerifiedRemote + import qualified Remote.External + import Remote.Helper.Encryptable (parseEncryptionMethod) + import Annex.Transfer +@@ -52,6 +53,7 @@ import Annex.Tmp + import Annex.SpecialRemote.Config + import Remote.List + import Remote.List.Util ++import Remote.Helper.ReadOnly + import Utility.Tmp + import Utility.Tmp.Dir + import Utility.Env +@@ -604,16 +606,19 @@ withSpecialRemote cfg@(SpecialRemoteConf + Logs.Remote.configSet u c' + setConfig (remoteConfig c' "url") (specialRemoteUrl cfg) + case M.lookup (Accepted "config-uuid") (specialRemoteConfig cfg) of +- Just cu -> do ++ Just pcu -> do ++ let cu = toUUID (fromProposedAccepted pcu) + setConfig (remoteAnnexConfig c' "config-uuid") +- (fromProposedAccepted cu) ++ (fromProposedAccepted pcu) + -- This is not quite the same as what is + -- usually stored to the git-annex branch + -- for the config-uuid, but it will work. + -- This change will never be committed to the + -- git-annex branch. +- Logs.Remote.configSet (toUUID (fromProposedAccepted cu)) c' ++ Logs.Remote.configSet cu c' ++ Logs.VerifiedRemote.configSet cu c' + Nothing -> noop ++ setRemoteConfigVerification remotename RemoteConfigVerified + remotesChanged + getEnabledSpecialRemoteByName remotename >>= \case + Just rmt -> return rmt +@@ -684,7 +689,9 @@ importTreeWorkAround rmt + | otherwise = do + m <- Logs.Remote.remoteConfigMap + r <- Remote.getRepo rmt +- remoteGen' adjustconfig m (Remote.remotetype rmt) r >>= \case ++ u <- getRepoUUID r ++ gc <- Annex.getRemoteGitConfig r ++ remoteGen' adjustconfig adjustReadOnly m (Remote.remotetype rmt) r gc u >>= \case + Just rmt' -> return rmt' + Nothing -> giveup "Failed to use importtree=yes remote." + where +--- git-annex-10.20250416.orig/Command/ConfigRemote.hs ++++ git-annex-10.20250416/Command/ConfigRemote.hs +@@ -55,7 +55,7 @@ startSpecialRemote :: Annex () -> Git.Re + startSpecialRemote = startSpecialRemote' "configremote" . performSpecialRemote + + performSpecialRemote :: Annex () -> PerformSpecialRemote +-performSpecialRemote precheck _ u _ c _ mcu = do ++performSpecialRemote precheck _ u _ _ c _ mcu = do + precheck + case mcu of + Nothing -> Logs.Remote.configSet u c +--- git-annex-10.20250416.orig/Command/EnableRemote.hs ++++ git-annex-10.20250416/Command/EnableRemote.hs +@@ -12,6 +12,7 @@ module Command.EnableRemote where + import Command + import qualified Annex + import qualified Logs.Remote ++import qualified Logs.VerifiedRemote + import qualified Types.Remote as R + import qualified Git + import qualified Git.Types as Git +@@ -22,10 +23,13 @@ import qualified Remote.Git + import qualified Command.InitRemote + import Logs.UUID + import Annex.UUID ++import Annex.Hook + import Config + import Config.DynamicConfig + import Types.GitConfig ++import Types.RemoteConfig + import Types.ProposedAccepted ++import Command.VerifyRemote (remoteVerificationMessage) + import Git.Config + + import qualified Data.Map as M +@@ -87,7 +91,7 @@ startNormalRemote name r = starting "ena + startSpecialRemote :: EnableRemoteOptions -> Git.RemoteName -> Remote.RemoteConfig -> [(UUID, Remote.RemoteConfig, Maybe (SpecialRemote.ConfigFrom UUID))] -> CommandStart + startSpecialRemote o = startSpecialRemote' "enableremote" (performSpecialRemote o) + +-type PerformSpecialRemote = RemoteType -> UUID -> R.RemoteConfig -> R.RemoteConfig -> RemoteGitConfig -> Maybe (SpecialRemote.ConfigFrom UUID) -> CommandPerform ++type PerformSpecialRemote = RemoteType -> UUID -> Git.RemoteName -> R.RemoteConfig -> R.RemoteConfig -> RemoteGitConfig -> Maybe (SpecialRemote.ConfigFrom UUID) -> CommandPerform + + startSpecialRemote' :: String -> PerformSpecialRemote -> Git.RemoteName -> Remote.RemoteConfig -> [(UUID, Remote.RemoteConfig, Maybe (SpecialRemote.ConfigFrom UUID))] -> CommandStart + startSpecialRemote' cname perform name config [] = do +@@ -105,7 +109,7 @@ startSpecialRemote' cname perform name c + gc <- maybe (liftIO dummyRemoteGitConfig) + (return . Remote.gitconfig) + =<< Remote.byUUID u +- perform t u c fullconfig gc mcu ++ perform t u name c fullconfig gc mcu + where + ai = ActionItemOther (Just (UnquotedString name)) + si = SeekInput [name] +@@ -113,27 +117,36 @@ startSpecialRemote' _ _ _ _ _ = + giveup "Multiple remotes have that name. Either use git-annex renameremote to rename them, or specify the uuid of the remote." + + performSpecialRemote :: EnableRemoteOptions -> PerformSpecialRemote +-performSpecialRemote o t u oldc c gc mcu = do +- -- Avoid enabling a special remote if there is another remote +- -- with the same name. +- case SpecialRemote.lookupName c of ++performSpecialRemote o t u name oldc c gc mcu = do ++ rs <- Remote.remoteList ++ prevrmt <- case filter (\rmt -> Remote.name rmt == name) rs of ++ (rmt:_) -> return (Just rmt) ++ _ -> return Nothing ++ unless (maybe u Remote.uuid prevrmt == u) $ ++ giveup $ "Not overwriting currently configured git remote named \"" ++ name ++ "\"" ++ enablePrePushHook >>= \case + Nothing -> noop +- Just name -> do +- rs <- Remote.remoteList +- case filter (\rmt -> Remote.name rmt == name) rs of +- (rmt:_) | Remote.uuid rmt /= u -> +- giveup $ "Not overwriting currently configured git remote named \"" ++ name ++ "\"" +- _ -> noop ++ Just msg -> giveup $ "Cannot enable this special remote because " ++ msg ++ let reenabling = isJust prevrmt + (c', u') <- R.setup t (R.Enable oldc) (Just u) Nothing c gc +- next $ cleanupSpecialRemote o t u' c' mcu ++ next $ cleanupSpecialRemote o t u' c' mcu reenabling name + +-cleanupSpecialRemote :: EnableRemoteOptions -> RemoteType -> UUID -> R.RemoteConfig -> Maybe (SpecialRemote.ConfigFrom UUID) -> CommandCleanup +-cleanupSpecialRemote o t u c mcu = do +- case mcu of +- Nothing -> Logs.Remote.configSet u c ++cleanupSpecialRemote :: EnableRemoteOptions -> RemoteType -> UUID -> R.RemoteConfig -> Maybe (SpecialRemote.ConfigFrom UUID) -> Bool -> Git.RemoteName -> CommandCleanup ++cleanupSpecialRemote o t u c mcu reenabling name = do ++ cu <- case mcu of ++ Nothing -> do ++ Logs.Remote.configSet u c ++ return u + Just (SpecialRemote.ConfigFrom cu) -> do + setConfig (remoteAnnexConfig c "config-uuid") (fromUUID cu) + Logs.Remote.configSet cu c ++ return cu ++ if reenabling ++ then Logs.VerifiedRemote.configUpdate cu c ++ else setRemoteConfigVerification name RemoteConfigUnverifiedManualEnabled ++ remoteVerificationMessage name cu c >>= \case ++ Just msg -> showLongNote (UnquotedString msg) ++ Nothing -> noop + Remote.byUUID u >>= \case + Nothing -> noop + Just r -> do +--- git-annex-10.20250416.orig/Command/Init.hs ++++ git-annex-10.20250416/Command/Init.hs +@@ -14,6 +14,7 @@ import Annex.Init + import Annex.Version + import Annex.Startup + import Types.RepoVersion ++import qualified Annex + import qualified Annex.SpecialRemote + + import Control.Monad.Fail as Fail (MonadFail(..)) +@@ -72,20 +73,23 @@ start os + + perform :: InitOptions -> CommandPerform + perform os = do ++ startversion <- getVersion + case initVersion os of + Nothing -> noop +- Just wantversion -> getVersion >>= \case ++ Just wantversion -> case startversion of + Just v | v /= wantversion -> + giveup $ "This repository is already a initialized with version " ++ show (fromRepoVersion v) ++ ", not changing to requested version." + _ -> noop + initialize startupAnnex + (if null (initDesc os) then Nothing else Just (initDesc os)) + (initVersion os) +- unless (noAutoEnable os) ++ unless (noAutoEnable os || isJust startversion) $ + Annex.SpecialRemote.autoEnable + next $ return True + + performAutoEnableOnly :: CommandPerform + performAutoEnableOnly = do ++ unlessM (Annex.getRead Annex.force) $ ++ giveup "It is not safe to autoenable new special remotes if any other git-annex processes may be running. Pass --force if you are sure you want to do this." + Annex.SpecialRemote.autoEnable + next $ return True +--- git-annex-10.20250416.orig/Command/InitRemote.hs ++++ git-annex-10.20250416/Command/InitRemote.hs +@@ -16,7 +16,8 @@ import qualified Types.Remote as R + import Types.RemoteConfig + import Annex.UUID + import Logs.UUID +-import Logs.Remote ++import qualified Logs.Remote ++import qualified Logs.VerifiedRemote + import Types.GitConfig + import Types.ProposedAccepted + import Config +@@ -96,7 +97,7 @@ start o (name:ws) = do + (sameas o) + c <- newConfig name sameasuuid + (Logs.Remote.keyValToConfig Proposed ws) +- <$> remoteConfigMap ++ <$> Logs.Remote.remoteConfigMap + t <- either giveup return (findType c) + a c t + +@@ -128,10 +129,13 @@ cleanup t u name c o = do + Nothing -> do + describeUUID u (toUUIDDesc name) + Logs.Remote.configSet u c ++ Logs.VerifiedRemote.configSet u c + Just _ -> do + cu <- liftIO genUUID + setConfig (remoteAnnexConfig c "config-uuid") (fromUUID cu) + Logs.Remote.configSet cu c ++ Logs.VerifiedRemote.configSet cu c ++ setRemoteConfigVerification name RemoteConfigVerified + when (withUrl o) $ + setAnnexUrl c + unless (Remote.gitSyncableRemoteType t || withUrl o) $ +--- /dev/null ++++ git-annex-10.20250416/Command/PrePush.hs +@@ -0,0 +1,30 @@ ++{- git-annex command ++ - ++ - Copyright 2026 Joey Hess ++ - ++ - Licensed under the GNU AGPL version 3 or higher. ++ -} ++ ++module Command.PrePush where ++ ++import Command ++import Remote.List ++import Types.Remote ++import qualified Logs.Remote ++import Logs.VerifiedRemote ++ ++cmd :: Command ++cmd = noCommit $ command "pre-push" SectionPlumbing ++ "run by git pre-push hook" ++ paramPaths ++ (withParams seek) ++ ++seek :: CmdParams -> CommandSeek ++seek _ = do ++ rs <- map (\r -> (uuid r, gitconfig r, Just (name r))) <$> remoteList ++ m <- Logs.Remote.remoteConfigMap ++ unverifiedRemoteAmelioration rs m >>= \case ++ Nothing -> noop ++ Just amelioration -> ++ maybe noop giveup $ ++ preventGitPush amelioration +--- /dev/null ++++ git-annex-10.20250416/Command/VerifyRemote.hs +@@ -0,0 +1,258 @@ ++{- git-annex command ++ - ++ - Copyright 2026 Joey Hess ++ - ++ - Licensed under the GNU AGPL version 3 or higher. ++ -} ++ ++{-# LANGUAGE OverloadedStrings #-} ++ ++module Command.VerifyRemote where ++ ++import Command ++import qualified Logs.Remote ++import qualified Logs.VerifiedRemote ++import Logs.VerifiedRemote (VerifyingRemoteConfigId(..)) ++import qualified Remote ++import qualified Types.Remote as Remote ++import Types.RemoteConfig ++import Types.ProposedAccepted ++import Config ++import Annex.SpecialRemote.Config ++import Git.Types (RemoteName) ++ ++import qualified Data.Map as M ++import qualified Data.Set as S ++ ++cmd :: Command ++cmd = command "verifyremote" SectionSetup ++ "verify if a special remote is safe to use" ++ paramName ++ (seek <$$> optParser) ++ ++data VerifyRemoteOptions = VerifyRemoteOptions ++ { remote :: DeferredParse Remote ++ , safeunsafe :: Maybe SafeUnsafe ++ } ++ ++data SafeUnsafe = Safe VerifyingRemoteConfigId | Unsafe ++ ++optParser :: CmdParamsDesc -> Parser VerifyRemoteOptions ++optParser _ = VerifyRemoteOptions ++ <$> (mkParseRemoteOption <$> remotearg) ++ <*> optional safeUnsafeParser ++ where ++ remotearg = strArgument ++ ( metavar paramRemote ++ <> help "remote to verify" ++ <> completeRemotes ++ ) ++ ++safeUnsafeParser :: Parser SafeUnsafe ++safeUnsafeParser = safe <|> unsafe ++ where ++ safe = option (Safe . VerifyingRemoteConfigId <$> maybeReader readish) ++ ( long "safe" <> metavar "ID" ++ <> help "special remote configuration is safe" ++ ) ++ unsafe = flag' Unsafe ++ ( long "unsafe" ++ <> help "special remote configuration is unsafe" ++ ) ++ ++seek :: VerifyRemoteOptions -> CommandSeek ++seek = commandAction . start ++ ++start :: VerifyRemoteOptions -> CommandStart ++start o = do ++ r <- getParsed (remote o) ++ let si = SeekInput [Remote.name r] ++ let ai = ActionItemUUID ++ (Remote.uuid r) ++ (UnquotedString (Remote.name r)) ++ case safeunsafe o of ++ Just (Safe cid) -> starting "verifyremote" ai si $ ++ performVerifySafe cid r ++ Just Unsafe -> starting "verifyremote" ai si $ ++ performVerifyUnsafe r ++ Nothing -> startVerify r si ++ ++startVerify :: Remote -> SeekInput -> CommandStart ++startVerify r si = do ++ showCustom "verifyremote" si $ do ++ remoteconfig <- getUnverifiedRemoteConfig (logUUID r) ++ cid <- Logs.VerifiedRemote.storeVerifyingRemoteLog remoteconfig ++ verifiedremoteconfig <- getVerifiedRemoteConfig (logUUID r) ++ let msg = fromMaybe nochanges $ ++ remoteVerificationMessage' (Remote.name r) remoteconfig cid verifiedremoteconfig ++ showRaw (encodeBS msg) ++ return True ++ stop ++ where ++ nochanges = "There have been no changes to the remote's configuration since it was last verified." ++ ++remoteVerificationMessage :: RemoteName -> UUID -> RemoteConfig -> Annex (Maybe String) ++remoteVerificationMessage remotename u remoteconfig = ++ remoteVerificationMessage' remotename remoteconfig ++ <$> Logs.VerifiedRemote.storeVerifyingRemoteLog remoteconfig ++ <*> getVerifiedRemoteConfig u ++ ++-- Nothing when there are no changes to the RemoteConfig ++-- that need verification. ++remoteVerificationMessage' :: RemoteName -> RemoteConfig -> VerifyingRemoteConfigId -> RemoteConfig -> Maybe String ++remoteVerificationMessage' remotename remoteconfig cid verifiedremoteconfig ++ | visiblefields verifiedremoteconfig == visibleremoteconfig = Nothing ++ | otherwise = Just $ ++ if verifiedremoteconfig /= mempty ++ then updatemessage ++ else newmessage ++ where ++ newmessage = unlines ++ [ "This is the configuration of the remote:" ++ , "\t" ++ displayRemoteConfig visibleremoteconfig ++ , "" ++ , askexpected ++ , "If so, run: " ++ verifysafecommand ++ , "If not, run: " ++ verifyunsafecommand ++ ] ++ ++ updatemessage = unlines ++ [ "There is an updated configuration of the remote:" ++ , "\t" ++ displayRemoteConfig visibleremoteconfig ++ , "" ++ , askexpected ++ , "If so, run: " ++ verifysafecommand ++ , "If not, you do not need to do anything; the previously" ++ , "verified configuration of the remote will continue to" ++ , "be used." ++ ] ++ ++ askexpected = "Is this a location that you expected to be using?" ++ ++ verifysafecommand = "git-annex verifyremote " ++ ++ remotename ++ " --safe " ++ ++ show (fromVerifyingRemoteConfigId cid) ++ ++ verifyunsafecommand = "git-annex verifyremote " ++ ++ remotename ++ " --unsafe" ++ ++ visibleremoteconfig = visiblefields remoteconfig ++ ++ visiblefields = Logs.VerifiedRemote.omitUnverifiableFields ++ . flip M.withoutKeys omitfields ++ ++ -- Fields that do not need to be verified. ++ omitfields = S.fromList ++ [ nameField ++ , sameasNameField ++ , sameasUUIDField ++ ] ++ ++performVerifySafe :: VerifyingRemoteConfigId -> Remote -> CommandPerform ++performVerifySafe cid r = do ++ remoteconfig <- getUnverifiedRemoteConfig (logUUID r) ++ m <- Logs.VerifiedRemote.readVerifyingRemoteLog ++ case M.lookup cid m of ++ Just rc ++ | rc == Logs.VerifiedRemote.omitUnverifiableFields remoteconfig -> do ++ Logs.VerifiedRemote.configSet (logUUID r) remoteconfig ++ setRemoteConfigVerification r RemoteConfigVerified ++ Logs.VerifiedRemote.removeVerifyingLog cid ++ next $ return True ++ _ -> do ++ warning "invalid ID" ++ next $ return False ++ ++performVerifyUnsafe :: Remote -> CommandPerform ++performVerifyUnsafe r = do ++ m <- fromMaybe mempty <$> Logs.VerifiedRemote.readRemoteLog ++ let hasverifiedconfig = M.member (logUUID r) m ++ warning $ UnquotedString $ unlines $ ++ warningVerifyUnsafe ++ (Remote.name r) ++ hasverifiedconfig ++ (remoteAnnexVerified (Remote.gitconfig r)) ++ next $ return False ++ ++warningVerifyUnsafe :: RemoteName -> Bool -> RemoteConfigVerification -> [String] ++warningVerifyUnsafe name False RemoteConfigUnverifiedAutoEnabled = ++ [ "So the autoenabled remote " ++ name ++ " is in a location" ++ , "you did not expect to be using. You may be under attack." ++ , "" ++ , "No annexed files have been sent to that remote or any other" ++ , "remote since it was enabled, and git pushes to all remotes" ++ , "have also been prevented since that time." ++ , "" ++ , "But, annexed files may have been copied from that remote into" ++ , "the local repository. There is a risk that those files could" ++ , "be sent on to other remotes, or that information in the git-annex" ++ , "branch could be pushed later, leaking information about the" ++ , "content of " ++ name ++ "." ++ , "" ++ , "Also, if the remote supports git pulls, branches may have been" ++ , "pulled from it into this repository, which may leak data" ++ , "to it when pushed elsewhere." ++ , "" ++ , "Your options for dealing with this include:" ++ , "* Identify who caused " ++ name ++ " to be configured this way," ++ , " and remove any remotes under their control to avoid leaking data" ++ , " to them." ++ , "* Delete this local git repository and all its contents." ++ , " A good choice if you know you will not lose any work or data." ++ , "" ++ , "As long as the remote " ++ name ++ " exists, annexed files" ++ , "will continue to be blocked from being sent to any remote," ++ , "and git pushes will also be blocked. Once you have dealt" ++ , "with this attack, you can unblock by running:" ++ , "\tgit remote remove " ++ name ++ ] ++warningVerifyUnsafe name False RemoteConfigUnverifiedManualEnabled = ++ [ "So the remote " ++ name ++ " is in a location you did not expect" ++ , "to be using. You may be under attack." ++ , "" ++ , "No annexed files have been sent to or retrieved from the" ++ , "remote since it was enabled." ++ , "" ++ , "But, if the remote supports git fetch, branches may have been" ++ , "pulled from it into this repository. If you determine that" ++ , "git has fetched information from the remote, you may need to" ++ , "clean up those branches to avoid leaking information from the" ++ , "remote. Bear in mind that the git-annex branch could be amoung" ++ , "them." ++ , "" ++ , "Git pushes to all remotes have been blocked since remote" ++ , name ++ " was enabled, and will stay blocked as long as it exists." ++ , "Once you have dealt with this attack, you can unblock by running:" ++ , "\tgit remote remove " ++ name ++ ] ++warningVerifyUnsafe name _ _ = ++ [ "Unfortunately, information may have leaked from " ++ name ++ "." ++ , "" ++ , "Either it has already been verified as safe, or it was" ++ , "enabled by a previous version of git-annex that did not verify" ++ , "the location of special remotes. So nothing prevented" ++ , "information from it being sent to other remotes." ++ ] ++ ++getUnverifiedRemoteConfig :: UUID -> Annex RemoteConfig ++getUnverifiedRemoteConfig u = do ++ m <- Logs.Remote.readRemoteLog ++ return $ fromMaybe mempty (M.lookup u m) ++ ++getVerifiedRemoteConfig :: UUID -> Annex RemoteConfig ++getVerifiedRemoteConfig u = do ++ m <- fromMaybe mempty <$> Logs.VerifiedRemote.readRemoteLog ++ return $ fromMaybe mempty (M.lookup u m) ++ ++logUUID :: Remote -> UUID ++logUUID r = fromMaybe (Remote.uuid r) $ ++ remoteAnnexConfigUUID (Remote.gitconfig r) ++ ++displayRemoteConfig :: RemoteConfig -> String ++displayRemoteConfig = intercalate " " . map display . sortBy order . M.toList ++ where ++ display (f, v) = ++ fromProposedAccepted f ++ "=" ++ fromProposedAccepted v ++ order (f1, v1) (f2, v2) = ++ conventionalConfigOrder f1 f2 <> compare v1 v2 ++ +--- git-annex-10.20250416.orig/Config.hs ++++ git-annex-10.20250416/Config.hs +@@ -81,6 +81,12 @@ setRemoteIgnore r b = setConfig (remoteA + setRemoteBare :: Git.Repo -> Bool -> Annex () + setRemoteBare r b = setConfig (remoteAnnexConfig r "bare") (Git.Config.boolConfig b) + ++setRemoteConfigVerification :: RemoteNameable r => r -> RemoteConfigVerification -> Annex () ++setRemoteConfigVerification r = ++ maybe (unsetConfig c) (setConfig c) . formatRemoteConfigVerification ++ where ++ c = remoteAnnexConfig r "verified" ++ + isBareRepo :: Annex Bool + isBareRepo = fromRepo Git.repoIsLocalBare + --- git-annex-10.20250416.orig/Database/Keys.hs +++ git-annex-10.20250416/Database/Keys.hs @@ -1,6 +1,6 @@ @@ -88,3 +927,1092 @@ diff old new = -- Avoid running smudge clean filter, since we want the -- raw output, and it would block trying to access the +--- git-annex-10.20250416.orig/Logs/File.hs ++++ git-annex-10.20250416/Logs/File.hs +@@ -9,9 +9,11 @@ + + module Logs.File ( + writeLogFile, ++ writeLogFile', + withLogHandle, + appendLogFile, + modifyLogFile, ++ readLogFile, + streamLogFile, + streamLogFileUnsafe, + checkLogFile, +@@ -35,10 +37,13 @@ import qualified Data.ByteString.Lazy.Ch + -- making the new file have whatever permissions the git repository is + -- configured to use. Creates the parent directory when necessary. + writeLogFile :: OsPath -> String -> Annex () +-writeLogFile f c = createDirWhenNeeded f $ viaTmp writelog f c ++writeLogFile f c = writeLogFile' f (\tmp -> writeFile (fromOsPath tmp) c) ++ ++writeLogFile' :: OsPath -> (OsPath -> IO ()) -> Annex () ++writeLogFile' f writer = createDirWhenNeeded f $ viaTmp writelog f () + where +- writelog tmp c' = do +- liftIO $ writeFile (fromOsPath tmp) c' ++ writelog tmp () = do ++ liftIO $ writer tmp + setAnnexFilePerm tmp + + -- | Runs the action with a handle connected to a temp file. +@@ -85,6 +90,10 @@ modifyLogFile f lck modf = withExclusive + liftIO $ F.writeFile lf b + setAnnexFilePerm lf + ++-- | Reads a log file if it exists. ++readLogFile :: OsPath -> OsPath -> Annex (Maybe L.ByteString) ++readLogFile f lck = withSharedLock lck $ liftIO $ tryWhenExists $ F.readFile f ++ + -- | Checks the content of a log file to see if any line matches. + checkLogFile :: OsPath -> OsPath -> (L.ByteString -> Bool) -> Annex Bool + checkLogFile f lck matchf = withSharedLock lck $ bracket setup cleanup go +--- git-annex-10.20250416.orig/Logs/Remote.hs ++++ git-annex-10.20250416/Logs/Remote.hs +@@ -25,6 +25,7 @@ import Types.Remote + import Logs + import Logs.UUIDBased + import Logs.Remote.Pure ++import qualified Logs.VerifiedRemote + import Annex.SpecialRemote.Config + + import qualified Data.Map as M +@@ -39,18 +40,28 @@ configSet u cfg = do + . parseRemoteConfigLog + Annex.changeState $ \s -> s { Annex.remoteconfigmap = Nothing } + +-{- Map of remotes by uuid containing key/value config maps. +- - Cached for speed. -} ++{- Map of remotes by uuid with their RemoteConfigs. Cached for speed. ++ - ++ - When the verified remote log has a RemoteConfig, it is used, otherwise ++ - the value from the git-annex branch log is used. This ensures that, once ++ - a user has verified a RemoteConfig, it does not change unexpectedly. ++ - ++ - Fields that are not verifiable are always taken from the git-annex ++ - branch log. ++ -} + remoteConfigMap :: Annex (M.Map UUID RemoteConfig) +-remoteConfigMap = maybe remoteConfigMapLoad return +- =<< Annex.getState Annex.remoteconfigmap +- +-remoteConfigMapLoad :: Annex (M.Map UUID RemoteConfig) +-remoteConfigMapLoad = do +- m <- readRemoteLog +- Annex.changeState $ \s -> s { Annex.remoteconfigmap = Just m } +- return m ++remoteConfigMap = maybe load return =<< Annex.getState Annex.remoteconfigmap ++ where ++ load = do ++ logged <- readRemoteLog ++ verified <- Logs.VerifiedRemote.addUnverifiableFields logged ++ . fromMaybe mempty ++ <$> Logs.VerifiedRemote.remoteConfigMap ++ let m = M.union verified logged ++ Annex.changeState $ \s -> ++ s { Annex.remoteconfigmap = Just m } ++ return m + + readRemoteLog :: Annex (M.Map UUID RemoteConfig) +-readRemoteLog = calcRemoteConfigMap ++readRemoteLog = calcRemoteConfigMap + <$> Annex.Branch.get remoteLog +--- git-annex-10.20250416.orig/Logs/Remote/Pure.hs ++++ git-annex-10.20250416/Logs/Remote/Pure.hs +@@ -7,10 +7,13 @@ + + module Logs.Remote.Pure ( + calcRemoteConfigMap, ++ calcRemoteConfigMapBase, + parseRemoteConfigLog, + buildRemoteConfigLog, ++ remoteConfigParser, + keyValToConfig, + configToKeyVal, ++ buildRemoteConfig, + showConfig, + + prop_isomorphic_configEscape, +@@ -32,18 +35,23 @@ import Data.ByteString.Builder + + calcRemoteConfigMap :: L.ByteString -> M.Map UUID RemoteConfig + calcRemoteConfigMap = (\m -> M.map (addSameasInherited m) m) +- . simpleMap +- . parseRemoteConfigLog ++ . calcRemoteConfigMapBase ++ ++calcRemoteConfigMapBase :: L.ByteString -> M.Map UUID RemoteConfig ++calcRemoteConfigMapBase = simpleMap . parseRemoteConfigLog + + parseRemoteConfigLog :: L.ByteString -> Log RemoteConfig + parseRemoteConfigLog = parseLogOld remoteConfigParser + + buildRemoteConfigLog :: Log RemoteConfig -> Builder +-buildRemoteConfigLog = buildLogOld (byteString . encodeBS . showConfig) ++buildRemoteConfigLog = buildLogOld buildRemoteConfig + + remoteConfigParser :: A.Parser RemoteConfig + remoteConfigParser = keyValToConfig Accepted . words . decodeBS <$> A.takeByteString + ++buildRemoteConfig :: RemoteConfig -> Builder ++buildRemoteConfig = byteString . encodeBS . showConfig ++ + showConfig :: RemoteConfig -> String + showConfig = unwords . configToKeyVal + +--- /dev/null ++++ git-annex-10.20250416/Logs/VerifiedRemote.hs +@@ -0,0 +1,277 @@ ++{- git-annex verified remote log ++ - ++ - This is similar to the git-annex remote log, but only remote configs ++ - that have been verified by the user to be safe to use. ++ - ++ - Copyright 2026 Joey Hess ++ - ++ - Licensed under the GNU AGPL version 3 or higher. ++ -} ++ ++{-# LANGUAGE TupleSections #-} ++ ++module Logs.VerifiedRemote ( ++ remoteConfigMap, ++ readRemoteLog, ++ configSet, ++ configUpdate, ++ omitUnverifiableFields, ++ addUnverifiableFields, ++ VerifyingRemoteConfigId(..), ++ UnverifiedRemoteAmelioration(..), ++ unverifiedRemoteAmelioration, ++ readVerifyingRemoteLog, ++ storeVerifyingRemoteLog, ++ removeVerifyingLog ++) where ++ ++import Annex.Common ++import qualified Annex ++import Types.Remote ++import Types.VectorClock ++import Logs.File ++import Logs.UUIDBased ++import Logs.Remote.Pure ++import Logs.MapLog ++import Annex.SpecialRemote.Config ++import qualified Utility.FileIO as F ++import Git.Types (RemoteName) ++ ++import qualified Data.Map as M ++import qualified Data.ByteString.Lazy as L ++import qualified Data.Attoparsec.ByteString.Char8 as A8 ++import Data.ByteString.Builder ++ ++configSet :: UUID -> RemoteConfig -> Annex () ++configSet u c = configSetMulti [(u, c)] ++ ++{- Only sets the RemoteConfig if the log already contains a RemoteConfig ++ - for this UUID. -} ++configUpdate :: UUID -> RemoteConfig -> Annex () ++configUpdate u c = configSetMulti' (M.member u . fromMapLog) [(u, c)] ++ ++configSetMulti :: [(UUID, RemoteConfig)] -> Annex () ++configSetMulti = configSetMulti' (const True) ++ ++configSetMulti' ++ :: (Log RemoteConfig -> Bool) ++ -> [(UUID, RemoteConfig)] ++ -> Annex () ++configSetMulti' shouldset ucfgs = do ++ c <- currentVectorClock ++ logf <- fromRepo gitAnnexVerifiedRemoteLog ++ lck <- fromRepo gitAnnexVerifiedRemoteLock ++ old <- parseRemoteConfigLog . fromMaybe mempty ++ <$> readLogFile logf lck ++ writeLogFile' logf $ \tmp -> F.writeFile tmp $ ++ toLazyByteString $ buildRemoteConfigLog $ ++ foldl' (accumchange c) old ucfgs ++ Annex.changeState $ \s -> ++ s { Annex.verifiedremoteconfigmap = Nothing } ++ where ++ accumchange c old (u, cfg) ++ | shouldset old = ++ changeLog c u (omitUnverifiableFields cfg) old ++ | otherwise = old ++ ++{- Hidden encryption fields cannot be verified by the user, and in any ++ - case do not configure the location that the remote accesses, and so ++ - are omitted from this log file. ++ -} ++omitUnverifiableFields :: RemoteConfig -> RemoteConfig ++omitUnverifiableFields c = M.withoutKeys c hiddenEncryptionFields ++ ++addUnverifiableFields ++ :: M.Map UUID RemoteConfig ++ -> M.Map UUID RemoteConfig ++ -> M.Map UUID RemoteConfig ++addUnverifiableFields unverifiedconfigs verifiedconfigs = ++ M.mapWithKey go verifiedconfigs ++ where ++ go u verifiedconfig = case M.lookup u unverifiedconfigs of ++ Just unverifiedconfig -> ++ let unverifiable = M.restrictKeys ++ unverifiedconfig ++ hiddenEncryptionFields ++ in M.union unverifiable verifiedconfig ++ Nothing -> verifiedconfig ++ ++{- Map of remotes by uuid with their RemoteConfigs. Cached for speed. ++ - ++ - Nothing when the verified remote config log file does not exist yet. ++ -} ++remoteConfigMap :: Annex (Maybe (M.Map UUID RemoteConfig)) ++remoteConfigMap = ++ maybe load return =<< Annex.getState Annex.verifiedremoteconfigmap ++ where ++ load = do ++ m <- readRemoteLog ++ Annex.changeState $ \s -> ++ s { Annex.verifiedremoteconfigmap = Just m } ++ return m ++ ++readRemoteLog :: Annex (Maybe (M.Map UUID RemoteConfig)) ++readRemoteLog = do ++ logf <- fromRepo gitAnnexVerifiedRemoteLog ++ lck <- fromRepo gitAnnexVerifiedRemoteLock ++ fmap calcRemoteConfigMapBase <$> readLogFile logf lck ++ ++data UnverifiedRemoteAmelioration = UnverifiedRemoteAmelioration ++ { preventGitPush :: Maybe String ++ -- ^ Prevent git push to all remotes. ++ , preventAnnexWrite :: Maybe String ++ -- ^ Prevent any git-annex object modification of all remotes. ++ , preventAnnexAccess :: UUID -> Maybe String ++ -- ^ Prevent any git-annex object access to remotes whose UUID ++ -- the function selects. The returned message explains why. ++ } ++ ++{- Given a list of UUIDs of all currently enabled remotes, check if any of ++ - them are not in the verified remote log, and depending on their ++ - remote.name.unverified configuration, returns the necessary ++ - amelioration to prevent exfiltation of data from an unverified remote ++ - to an attacker controlled remote. ++ -} ++unverifiedRemoteAmelioration ++ :: [(UUID, RemoteGitConfig, Maybe RemoteName)] ++ -> (M.Map UUID RemoteConfig) ++ -- ^ Logs.Remote.remoteConfigMap ++ -> Annex (Maybe UnverifiedRemoteAmelioration) ++unverifiedRemoteAmelioration rs configmap = remoteConfigMap >>= \case ++ Just m -> return $ ++ let l = mapMaybe (notinmap m) rs ++ in Just $ UnverifiedRemoteAmelioration ++ { preventGitPush = ++ checkprevented pushblocker l "Push" ++ , preventAnnexWrite = ++ checkprevented writeblocker l "Write" ++ , preventAnnexAccess = ++ accessblocker l ++ } ++ ++ -- The log file does not exist yet. This is either a newly ++ -- initialized repository, or git-annex has been upgraded from a ++ -- version that did not use this log. ++ -- ++ -- Handle upgrade by populating the log with the ++ -- current configs of all the currently enabled remotes. ++ -- ++ -- In a newly initialized repository, there are not yet ++ -- any enabled remotes, and this creates an empty log file. ++ Nothing -> do ++ configSetMulti $ mapMaybe (copyfromconfigmap configmap) rs ++ return Nothing ++ where ++ -- Any special remote that is unverified prevents git push to all ++ -- remotes. ++ pushblocker (_, gc, remotename) ++ | remoteAnnexVerified gc /= RemoteConfigVerified = remotename ++ | otherwise = Nothing ++ ++ -- Any autoenabled special remote that is unverified prevents ++ -- annex object write to all remotes. ++ writeblocker (_, gc, remotename) ++ | remoteAnnexVerified gc == RemoteConfigUnverifiedAutoEnabled = remotename ++ | remoteAnnexVerified gc == RemoteConfigUnverified = remotename ++ | otherwise = Nothing ++ ++ -- Prevent annex object access to any special remote that was ++ -- enabled manually but is not yet verified. ++ accessblocker l u = case filter (\(u',_, _) -> u' == u) l of ++ ((_, gc, Just remotename):_) ++ | remoteAnnexVerified gc == RemoteConfigUnverifiedManualEnabled -> ++ preventedunverified remotename ++ | remoteAnnexVerified gc == RemoteConfigUnverified -> ++ preventedunverified remotename ++ _ -> Nothing ++ ++ prevented remotename msg = Just $ unwords ++ [ msg ++ , "Run this command to verify it:" ++ , "git-annex verifyremote" ++ , remotename ++ ] ++ ++ preventdueto remotetoverify action = ++ prevented remotetoverify $ unwords ++ [ action ++ , "prevented due to an unverified special" ++ , "remote." ++ ] ++ ++ preventedunverified remotename = ++ prevented remotename $ ++ "This special remote cannot be used since " ++ ++ "it's not verified." ++ ++ checkprevented blocker l action = ++ case mapMaybe blocker l of ++ [] -> Nothing ++ (remotename:_) -> preventdueto remotename action ++ ++ notinmap m i@(u, gc, _) ++ | M.notMember u' configmap = Nothing ++ | M.member u' m = Nothing ++ | otherwise = Just i ++ where ++ u' = getcu u gc ++ ++ copyfromconfigmap m (u, gc, _) = ++ (getcu u gc, ) <$> M.lookup u m ++ ++ getcu u gc = fromMaybe u (remoteAnnexConfigUUID gc) ++ ++data VerifyingRemoteConfigId = VerifyingRemoteConfigId ++ { fromVerifyingRemoteConfigId :: Integer } ++ deriving (Eq, Ord) ++ ++readVerifyingRemoteLog :: Annex (M.Map VerifyingRemoteConfigId RemoteConfig) ++readVerifyingRemoteLog = do ++ logf <- fromRepo gitAnnexVerifyingRemoteLog ++ lck <- fromRepo gitAnnexVerifyingRemoteLock ++ simpleMap . parseVerifyingRemoteLog . fromMaybe mempty ++ <$> readLogFile logf lck ++ ++storeVerifyingRemoteLog :: RemoteConfig -> Annex VerifyingRemoteConfigId ++storeVerifyingRemoteLog cfg = ++ modifyVerifyingRemoteLog $ \c n -> ++ changeMapLog c n (omitUnverifiableFields cfg) ++ ++removeVerifyingLog :: VerifyingRemoteConfigId -> Annex () ++removeVerifyingLog n = ++ void $ modifyVerifyingRemoteLog ++ (\_ _ (MapLog m) -> MapLog (M.delete n m)) ++ ++modifyVerifyingRemoteLog ++ :: (CandidateVectorClock -> VerifyingRemoteConfigId -> MapLog VerifyingRemoteConfigId RemoteConfig -> MapLog VerifyingRemoteConfigId RemoteConfig) ++ -> Annex VerifyingRemoteConfigId ++modifyVerifyingRemoteLog f = do ++ c <- currentVectorClock ++ logf <- fromRepo gitAnnexVerifyingRemoteLog ++ lck <- fromRepo gitAnnexVerifyingRemoteLock ++ old <- parseVerifyingRemoteLog . fromMaybe mempty ++ <$> readLogFile logf lck ++ let nextcid = VerifyingRemoteConfigId $ succ $ ++ maybe 0 fromVerifyingRemoteConfigId $ ++ headMaybe $ reverse $ M.keys $ simpleMap old ++ writeLogFile' logf $ \tmp -> F.writeFile tmp $ ++ toLazyByteString $ buildVerifyingRemoteLog (f c nextcid old) ++ return nextcid ++ ++parseVerifyingRemoteLog ++ :: L.ByteString ++ -> MapLog VerifyingRemoteConfigId RemoteConfig ++parseVerifyingRemoteLog = ++ parseMapLog parseVerifyingRemoteConfigId remoteConfigParser ++ ++buildVerifyingRemoteLog ++ :: MapLog VerifyingRemoteConfigId RemoteConfig ++ -> Builder ++buildVerifyingRemoteLog = ++ buildMapLog buildVerifyingRemoteConfigId buildRemoteConfig ++ ++parseVerifyingRemoteConfigId :: A8.Parser VerifyingRemoteConfigId ++parseVerifyingRemoteConfigId = VerifyingRemoteConfigId <$> A8.decimal ++ ++buildVerifyingRemoteConfigId :: VerifyingRemoteConfigId -> Builder ++buildVerifyingRemoteConfigId = integerDec . fromVerifyingRemoteConfigId +--- git-annex-10.20250416.orig/NEWS ++++ git-annex-10.20250416/NEWS +@@ -1,3 +1,16 @@ ++git-annex (10.20250416.1) upstream; urgency=high ++ ++ A security fix has changed git-annex's behavior in several ways when ++ enabling special remotes. The new git-annex verify command needs to be ++ used to verify that the configuration of a special remote is safe. ++ git-annex will prompt in situations where that is needed. ++ ++ Also, changes to a special remote configuration received in the git-annex ++ branch no longer take effect until git-annex verifyremote is run to review ++ and verify the new configuration. ++ ++ -- Joey Hess Fri, 16 Oct 2026 09:29:27 -0400 ++ + git-annex (10.20230626) upstream; urgency=medium + + Many commands now quote filenames that contain unusual characters the +--- git-annex-10.20250416.orig/Remote/External.hs ++++ git-annex-10.20250416/Remote/External.hs +@@ -76,9 +76,9 @@ gen rt externalprogram r u rc gc rs + exportUnsupported + exportUnsupported + return $ Just $ specialRemote c +- readonlyStorer ++ (readonlyStorer readonlyWarning) + (retrieveUrl gc) +- readonlyRemoveKey ++ (readonlyRemoveKey readonlyWarning) + (checkKeyUrl gc) + rmt + | otherwise = do +--- git-annex-10.20250416.orig/Remote/Helper/ReadOnly.hs ++++ git-annex-10.20250416/Remote/Helper/ReadOnly.hs +@@ -7,9 +7,11 @@ + + module Remote.Helper.ReadOnly + ( adjustReadOnly ++ , forceReadOnly + , readonlyStoreKey + , readonlyStorer + , readonlyRemoveKey ++ , readonlyWarning + ) where + + import Annex.Common +@@ -26,50 +28,60 @@ import Utility.Metered + -} + adjustReadOnly :: Remote -> Remote + adjustReadOnly r +- | remoteAnnexReadOnly (gitconfig r) = r +- { storeKey = readonlyStoreKey +- , removeKey = readonlyRemoveKey +- , repairRepo = Nothing +- , exportActions = (exportActions r) +- { storeExport = readonlyStoreExport +- , removeExport = readonlyRemoveExport +- , removeExportDirectory = Just readonlyRemoveExportDirectory +- , renameExport = Nothing +- } +- , importActions = (importActions r) +- { storeExportWithContentIdentifier = readonlyStoreExportWithContentIdentifier +- , removeExportWithContentIdentifier = readonlyRemoveExportWithContentIdentifier +- , removeExportDirectoryWhenEmpty = Just readonlyRemoveExportDirectory +- } +- } ++ | remoteAnnexReadOnly (gitconfig r) = makeReadOnly readonlyWarning r + | otherwise = r + +-readonlyStoreKey :: Key -> AssociatedFile -> Maybe OsPath -> MeterUpdate -> Annex () +-readonlyStoreKey _ _ _ _ = readonlyFail ++{- Force a remote that would not usually be read-only to be read-only, ++ - with a message explaining why. -} ++forceReadOnly :: String -> Remote -> Remote ++forceReadOnly msg r ++ | remoteAnnexReadOnly (gitconfig r) = makeReadOnly readonlyWarning r ++ | otherwise = makeReadOnly msg r ++ ++makeReadOnly :: String -> Remote -> Remote ++makeReadOnly msg r = r ++ { storeKey = readonlyStoreKey msg ++ , removeKey = readonlyRemoveKey msg ++ , repairRepo = Nothing ++ , exportActions = (exportActions r) ++ { storeExport = readonlyStoreExport msg ++ , removeExport = readonlyRemoveExport msg ++ , removeExportDirectory = Just (readonlyRemoveExportDirectory msg) ++ , renameExport = Nothing ++ } ++ , importActions = (importActions r) ++ { storeExportWithContentIdentifier = readonlyStoreExportWithContentIdentifier msg ++ , removeExportWithContentIdentifier = readonlyRemoveExportWithContentIdentifier msg ++ , removeExportDirectoryWhenEmpty = Just (readonlyRemoveExportDirectory msg) ++ } ++ } ++ ++readonlyStoreKey :: String -> Key -> AssociatedFile -> Maybe OsPath -> MeterUpdate -> Annex () ++readonlyStoreKey msg _ _ _ _ = readonlyFail msg + +-readonlyRemoveKey :: Maybe SafeDropProof -> Key -> Annex () +-readonlyRemoveKey _ _ = readonlyFail ++readonlyRemoveKey :: String -> Maybe SafeDropProof -> Key -> Annex () ++readonlyRemoveKey msg _ _ = readonlyFail msg + +-readonlyStorer :: Storer +-readonlyStorer _ _ _ = readonlyFail ++readonlyStorer :: String -> Storer ++readonlyStorer msg _ _ _ = readonlyFail msg + +-readonlyStoreExport :: OsPath -> Key -> ExportLocation -> MeterUpdate -> Annex () +-readonlyStoreExport _ _ _ _ = readonlyFail ++readonlyStoreExport :: String -> OsPath -> Key -> ExportLocation -> MeterUpdate -> Annex () ++readonlyStoreExport msg _ _ _ _ = readonlyFail msg + +-readonlyRemoveExport :: Key -> ExportLocation -> Annex () +-readonlyRemoveExport _ _ = readonlyFail ++readonlyRemoveExport :: String -> Key -> ExportLocation -> Annex () ++readonlyRemoveExport msg _ _ = readonlyFail msg + +-readonlyRemoveExportDirectory :: ExportDirectory -> Annex () +-readonlyRemoveExportDirectory _ = readonlyFail ++readonlyRemoveExportDirectory :: String -> ExportDirectory -> Annex () ++readonlyRemoveExportDirectory msg _ = readonlyFail msg + +-readonlyStoreExportWithContentIdentifier :: OsPath -> Key -> ExportLocation -> [ContentIdentifier] -> MeterUpdate -> Annex ContentIdentifier +-readonlyStoreExportWithContentIdentifier _ _ _ _ _ = readonlyFail ++readonlyStoreExportWithContentIdentifier :: String -> OsPath -> Key -> ExportLocation -> [ContentIdentifier] -> MeterUpdate -> Annex ContentIdentifier ++readonlyStoreExportWithContentIdentifier msg _ _ _ _ _ = readonlyFail msg + +-readonlyRemoveExportWithContentIdentifier :: Key -> ExportLocation -> [ContentIdentifier] -> Annex () +-readonlyRemoveExportWithContentIdentifier _ _ _ = readonlyFail ++readonlyRemoveExportWithContentIdentifier :: String -> Key -> ExportLocation -> [ContentIdentifier] -> Annex () ++readonlyRemoveExportWithContentIdentifier msg _ _ _ = readonlyFail msg + +-readonlyFail :: Annex a +-readonlyFail = giveup readonlyWarning ++readonlyFail :: String -> Annex a ++readonlyFail = giveup + + readonlyWarning :: String + readonlyWarning = "this remote is readonly" +--- git-annex-10.20250416.orig/Remote/List.hs ++++ git-annex-10.20250416/Remote/List.hs +@@ -11,7 +11,8 @@ import qualified Data.Map as M + + import Annex.Common + import qualified Annex +-import Logs.Remote ++import qualified Logs.Remote ++import Logs.VerifiedRemote + import Types.Remote + import Types.RemoteState + import Annex.UUID +@@ -80,41 +81,92 @@ remoteList = do + + remoteList' :: Bool -> Annex [Remote] + remoteList' autoinit = do +- m <- remoteConfigMap +- rs <- concat <$> mapM (process m) remoteTypes ++ m <- Logs.Remote.remoteConfigMap ++ l <- concat <$> mapM enumerate' remoteTypes ++ ++ let usnames = map (\(u, g, gc, _) -> (u, gc, Git.remoteName g)) l ++ adjustremote <- mkadjustremote ++ <$> unverifiedRemoteAmelioration usnames m ++ ++ rs <- catMaybes <$> mapM (generate' adjustremote m) l + Annex.changeState $ \s -> s { Annex.remotes = rs } + return rs + where +- process m t = enumerate t autoinit +- >>= mapM (remoteGen m t) +- >>= return . catMaybes ++ enumerate' t = do ++ gs <- enumerate t autoinit ++ forM gs $ \g -> do ++ u <- getRepoUUID g ++ gc <- Annex.getRemoteGitConfig g ++ return (u, g, gc, t) ++ ++ generate' adjustremote m (u, g, gc, t) = ++ remoteGen adjustremote m t g gc u ++ ++ mkadjustremote Nothing = adjustReadOnly ++ mkadjustremote (Just amelioration) = \r -> ++ case preventAnnexAccess amelioration (uuid r) of ++ Just msg -> preventaccess r msg ++ Nothing -> ++ case preventAnnexWrite amelioration of ++ Just msg -> forceReadOnly msg r ++ Nothing -> adjustReadOnly r ++ ++ preventaccess r msg = ++ let ro = forceReadOnly msg r ++ in ro ++ { retrieveKeyFile = giveup msg ++ , retrieveKeyFileCheap = Nothing ++ , lockContent = Nothing ++ , checkPresent = giveup msg ++ , exportActions = (exportActions ro) ++ { retrieveExport = giveup msg ++ , checkPresentExport = giveup msg ++ } ++ , importActions = (importActions ro) ++ { listImportableContents = giveup msg ++ , importKey = giveup msg ++ , retrieveExportWithContentIdentifier = giveup msg ++ , checkPresentExportWithContentIdentifier = giveup msg ++ } ++ , claimUrl = Nothing ++ , checkUrl = Nothing ++ } + + {- Generates a Remote. -} +-remoteGen :: M.Map UUID RemoteConfig -> RemoteType -> Git.Repo -> Annex (Maybe Remote) ++remoteGen ++ :: (Remote -> Remote) ++ -> M.Map UUID RemoteConfig ++ -> RemoteType ++ -> Git.Repo ++ -> RemoteGitConfig ++ -> UUID ++ -> Annex (Maybe Remote) + remoteGen = remoteGen' id + + remoteGen' + :: (RemoteConfig -> RemoteConfig) ++ -> (Remote -> Remote) + -> M.Map UUID RemoteConfig + -> RemoteType + -> Git.Repo ++ -> RemoteGitConfig ++ -> UUID + -> Annex (Maybe Remote) +-remoteGen' adjustconfig m t g = do +- u <- getRepoUUID g +- gc <- Annex.getRemoteGitConfig g ++remoteGen' adjustconfig adjustremote m t g gc u = do + let cu = fromMaybe u $ remoteAnnexConfigUUID gc + let rs = RemoteStateHandle cu + let c = adjustconfig (fromMaybe M.empty $ M.lookup cu m) + generate t g u c gc rs >>= \case + Nothing -> return Nothing +- Just r -> Just <$> adjustExportImport (adjustReadOnly (addHooks r)) rs ++ Just r -> Just <$> adjustExportImport (adjustremote (addHooks r)) rs + + {- Updates a local git Remote, re-reading its git config. -} + updateRemote :: Remote -> Annex (Maybe Remote) + updateRemote remote = do +- m <- remoteConfigMap +- remote' <- updaterepo =<< getRepo remote +- remoteGen m (remotetype remote) remote' ++ m <- Logs.Remote.remoteConfigMap ++ g <- updaterepo =<< getRepo remote ++ gc <- Annex.getRemoteGitConfig g ++ remoteGen adjustReadOnly m (remotetype remote) g gc (uuid remote) + where + updaterepo r + | Git.repoIsLocal r || Git.repoIsLocalUnknown r = +--- git-annex-10.20250416.orig/Test.hs ++++ git-annex-10.20250416/Test.hs +@@ -364,6 +364,7 @@ repoTests note numparts = map mk $ sep + , testCase "add subdirs" test_add_subdirs + , testCase "addurl" test_addurl + , testCase "repair" test_repair ++ , testCase "autoenable requires verifyremote" test_autoenable_verifyremote + ] + where + mk l = testGroup groupname (initTests : map adddep l) +@@ -2180,3 +2181,24 @@ test_repair :: Assertion + test_repair = intmpclonerepo $ + -- Simply running repair used to fail on Windows. + git_annex "repair" [] "repair" ++ ++test_autoenable_verifyremote :: Assertion ++test_autoenable_verifyremote = ++ withtmpclonerepo $ \r1 -> do ++ withtmpclonerepo $ \r3 -> do ++ intopdir r1 $ do ++ git_annex "get" [] "get" ++ git_annex "pre-push" [""] "pre-push succeeds normally" ++ git_annex "initremote" ["baz", "type=git", "autoenable=true", "location=../"++r3] "initremote" ++ git_annex "pre-push" [""] "pre-push succeeds after initremote" ++ withtmpcloneof r1 $ \r2 -> do ++ intopdir r2 $ do ++ git_annex_shouldfail "move" ["--from=origin"] ++ "annex write is prevented by unverified autoenabled remote" ++ git_annex_shouldfail "pre-push" [""] ++ "pre-push blocks with unverified autoenabled remote" ++ git_shouldfail "push" ["r1"] ++ "git push is prevented by unverified autoenabled remote" ++ git_annex "enableremote" ["baz", "location=../"++r3] "enableremote" ++ git_annex_shouldfail "move" ["--from=origin"] ++ "annex write is prevented by unverified autoenabled remote after it was manually enabled too" +--- git-annex-10.20250416.orig/Test/Framework.hs ++++ git-annex-10.20250416/Test/Framework.hs +@@ -187,11 +187,18 @@ intmpsharedclonerepo a = withtmpclonerep + withtmpclonerepo :: (FilePath -> Assertion) -> Assertion + withtmpclonerepo = withtmpclonerepo' newCloneRepoConfig + ++withtmpcloneof :: FilePath -> (FilePath -> Assertion) -> Assertion ++withtmpcloneof = withtmpclonerepo'' newCloneRepoConfig ++ + withtmpclonerepo' :: CloneRepoConfig -> (FilePath -> Assertion) -> Assertion + withtmpclonerepo' cfg a = do ++ origindir <- mainrepodir ++ withtmpclonerepo'' cfg origindir a ++ ++withtmpclonerepo'' :: CloneRepoConfig -> FilePath -> (FilePath -> Assertion) -> Assertion ++withtmpclonerepo'' cfg origindir a = do + dir <- tmprepodir +- maindir <- mainrepodir +- clone <- clonerepo maindir dir cfg ++ clone <- clonerepo origindir dir cfg + r <- tryNonAsync (a clone) + case r of + Right () -> return () +--- git-annex-10.20250416.orig/Types/GitConfig.hs ++++ git-annex-10.20250416/Types/GitConfig.hs +@@ -51,6 +51,7 @@ import Types.RepoVersion + import Types.StallDetection + import Types.View + import Types.Cluster ++import Types.RemoteConfig + import Config.DynamicConfig + import Utility.HumanTime + import Utility.Gpg (GpgCmd, mkGpgCmd) +@@ -386,6 +387,7 @@ data RemoteGitConfig = RemoteGitConfig + , remoteAnnexPull :: Bool + , remoteAnnexPush :: Bool + , remoteAnnexReadOnly :: Bool ++ , remoteAnnexVerified :: RemoteConfigVerification + , remoteAnnexVerify :: Bool + , remoteAnnexCheckUUID :: Bool + , remoteAnnexTrackingBranch :: Maybe Git.Ref +@@ -464,6 +466,8 @@ extractRemoteGitConfig r remotename = do + , remoteAnnexPull = getbool PullField True + , remoteAnnexPush = getbool PushField True + , remoteAnnexReadOnly = getbool ReadOnlyField False ++ , remoteAnnexVerified = parseRemoteConfigVerification $ ++ getmaybe VerifiedField + , remoteAnnexCheckUUID = getbool CheckUUIDField True + , remoteAnnexVerify = getbool VerifyField True + , remoteAnnexTrackingBranch = Git.Ref . encodeBS <$> +@@ -572,6 +576,7 @@ data RemoteGitConfigField + | PullField + | PushField + | ReadOnlyField ++ | VerifiedField + | CheckUUIDField + | VerifyField + | TrackingBranchField +@@ -642,6 +647,7 @@ remoteGitConfigField = \case + PullField -> inherited True "pull" + PushField -> inherited True "push" + ReadOnlyField -> inherited True "readonly" ++ VerifiedField -> inherited True "verified" + CheckUUIDField -> uninherited True "checkuuid" + VerifyField -> inherited True "verify" + TrackingBranchField -> uninherited True "tracking-branch" +--- git-annex-10.20250416.orig/Types/RemoteConfig.hs ++++ git-annex-10.20250416/Types/RemoteConfig.hs +@@ -72,3 +72,31 @@ addRemoteConfigParser l rpc = rpc + where + s = S.fromList (map parserForField (remoteConfigFieldParsers rpc)) + isnew p = not (S.member (parserForField p) s) ++ ++{- Has a RemoteConfig been verified to use the location that the user ++ - expected? -} ++data RemoteConfigVerification ++ = RemoteConfigVerified ++ | RemoteConfigUnverifiedManualEnabled ++ | RemoteConfigUnverifiedAutoEnabled ++ | RemoteConfigUnverified ++ -- ^ one or the other, but which is not yet known ++ deriving (Eq) ++ ++parseRemoteConfigVerification :: Maybe String -> RemoteConfigVerification ++parseRemoteConfigVerification (Just "verified") = ++ RemoteConfigVerified ++parseRemoteConfigVerification (Just "manual") = ++ RemoteConfigUnverifiedManualEnabled ++parseRemoteConfigVerification (Just "automatic") = ++ RemoteConfigUnverifiedAutoEnabled ++parseRemoteConfigVerification _ = RemoteConfigUnverified ++ ++formatRemoteConfigVerification :: RemoteConfigVerification -> Maybe String ++formatRemoteConfigVerification RemoteConfigVerified = ++ Just "verified" ++formatRemoteConfigVerification RemoteConfigUnverifiedManualEnabled = ++ Just "manual" ++formatRemoteConfigVerification RemoteConfigUnverifiedAutoEnabled = ++ Just "automatic" ++formatRemoteConfigVerification RemoteConfigUnverified = Nothing +--- git-annex-10.20250416.orig/doc/git-annex-enableremote.mdwn ++++ git-annex-10.20250416/doc/git-annex-enableremote.mdwn +@@ -82,6 +82,8 @@ has found didn't work before and gave up + + [[git-annex-initremote]](1) + ++[[git-annex-verifyremote]](1) ++ + [[git-annex-configremote]](1) + + [[git-annex-renameremote]](1) +--- git-annex-10.20250416.orig/doc/git-annex-init.mdwn ++++ git-annex-10.20250416/doc/git-annex-init.mdwn +@@ -21,8 +21,10 @@ this will also attempt to enable them. S + To prevent that, re-enable a remote with "autoenable=false", or + mark it as dead (see [[git-annex-dead]](1)). + +-This command is entirely safe, although usually pointless, to run inside an +-already initialized git-annex repository. ++This command is entirely safe, although usually pointless, to run ++inside an already initialized git-annex repository. It avoids overwriting ++things like the uuid of the repository. It also avoids autoenabling any ++more special remotes, since it is not safe to do so. + + A top-level `.noannex` file will prevent git-annex init from being used + in a repository. This is useful for repositories that have a policy +--- /dev/null ++++ git-annex-10.20250416/doc/git-annex-pre-push.mdwn +@@ -0,0 +1,42 @@ ++# NAME ++ ++git-annex pre-push - run by git pre-push hook ++ ++# SYNOPSIS ++ ++git annex pre-push ++ ++# DESCRIPTION ++ ++This is meant to be called from git's pre-push hook. git-annex ++automatically sets up the hook. ++ ++This hook prevents all pushes to git remotes when there is an unverified ++special remote. ++ ++The reason why is to guard against an attack, where an attacker configures ++a special remote to access a location that you do not expect it to access. ++For example, it might access a location that you do not intend to share ++with anyone. Once such a special remote is enabled, objects from it can ++reach the local repository, but are prevented from being sent to other ++repositories. But, a git push would send git-annex branch information, ++which could verify to an attacker that you do possess particular objects in ++the special remote. The hook avoids that information leakage. ++ ++# OPTIONS ++ ++* The [[git-annex-common-options]](1) can be used. ++ ++# SEE ALSO ++ ++[[git-annex]](1) ++ ++[[git-annex-verifyremote]](1) ++ ++[[git-annex-enableremote]](1) ++ ++# AUTHOR ++ ++Joey Hess ++ ++Warning: Automatically converted into a man page by mdwn2man. Edit with care. +--- /dev/null ++++ git-annex-10.20250416/doc/git-annex-verifyremote.mdwn +@@ -0,0 +1,63 @@ ++# NAME ++ ++git-annex verifyremote - verify if a special remote is safe to use ++ ++# SYNOPSIS ++ ++git annex verifyremote `name` [--safe ID|--unsafe] ++ ++# DESCRIPTION ++ ++When a special remote was created by someone else, it's important to ++verify that the repository it's configured to use is in the location ++you expect. Otherwise, you risk information leaking out from the ++special remote to other remotes. ++ ++To prevent such data leakage, `git-annex enableremote` will display ++the configuration of the special remote, and prompt you to run this ++command. Until the special remote is verified, it cannot be used. ++ ++When a special remote is autoenabled by `git-annex init`, ++it can be used before it is verified. But, git-annex will prevent sending ++annexed files or git pushes to any remote, until this command is used ++to verify the autoenabled special remote. ++ ++This command can also be used when the configuration of a special remote ++has changed. The old configuration will continue to be used until ++this command is used to verify the new configuration. ++ ++When run with no options, this command displays the configuration of the ++special remote, and prompts you to verify it. ++ ++# OPTIONS ++ ++* `--safe ID` ++ ++ Use when you have verified that the configuration of the special ++ remote makes it use the location you expect. ++ ++ The ID will be provided to you by git-annex when displaying the ++ configuration of the special remote. ++ ++* `--unsafe` ++ ++ If the configuration of the special remote makes it use a location that ++ you did not expect, use this option. ++ ++* Also, the [[git-annex-common-options]](1) can be used. ++ ++# SEE ALSO ++ ++[[git-annex]](1) ++ ++[[git-annex-initremote]](1) ++ ++[[git-annex-enableremote]](1) ++ ++[[git-annex-pre-push]](1) ++ ++# AUTHOR ++ ++Joey Hess ++ ++Warning: Automatically converted into a man page by mdwn2man. Edit with care. +--- git-annex-10.20250416.orig/doc/git-annex.mdwn ++++ git-annex-10.20250416/doc/git-annex.mdwn +@@ -271,6 +271,12 @@ content from the key-value store. + + See [[git-annex-enableremote]](1) for details. + ++* `verifyremote name` ++ ++ Verify if a special remote is safe to use. ++ ++ See [[git-annex-verifyremote]](1) for details. ++ + * `configremote name [param=value ...]` + + Changes configuration of an existing special remote. +@@ -681,6 +687,13 @@ content from the key-value store. + + See [[git-annex-post-receive]](1) for details. + ++* `pre-push` ++ ++ This is meant to be called from git's post-receive hook. `git annex init` ++ automatically creates a pre-push hook using this. ++ ++ See [[git-annex-pre-push]](1) for details. ++ + * `lookupkey [file ...]` + + Looks up key used for file. +@@ -1856,6 +1869,17 @@ Remotes are configured using these setti + Can be used to tell git-annex if a remote is a bare repository + or not. Normally, git-annex determines this automatically. + ++* `remote..annex-verified` ++ ++ When a special remote has been verified by eg `git-annex verify`, ++ this is set to "verified". ++ ++ When a special remote has been autoenabled, but not yet verified, ++ this is set to "automatic". ++ ++ When a special remote has been enabled by `git-annex initremote` ++ but not yet verified, this config is set to "manual". ++ + * `remote..annex-availability` + + This configuration setting is no longer used. +--- /dev/null ++++ git-annex-10.20250416/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn +@@ -0,0 +1,66 @@ ++(FIXME: CVE number assignment) ++git-annex was vulnerable to a private data exfiltration attack. Joey Hess ++discovered this attack. ++ ++This security hole was fixed in git-annex (FIXME: version) ++ ++## attack descriptions ++ ++To perform these attacks, the attacker needs to have control over one of ++the remotes of the victim's git-annex repository. For example, they may ++provide a git-annex repository that the victim clones. ++ ++The attacker also needs to have a certian amount of information about the ++victim's private repository that they have obtained in some way. For ++example, the victim may have published a git repository but be keeping the ++annexed files private. ++ ++In the simplest form of the attack, the attacker provides a git-annex ++repository that includes in its tree annexed files that the attacker knows ++are present in the victim's private git-annex repository. When git-annex is ++initialized in the victim's clone, git-annex autoenables a special remote ++that the attacker has configured to access the victim's private git-annex ++repository. When the victim runs a command such as `git-annex sync ++--content`, their private files are retrieved from the special remote and ++sent to the attacker. ++ ++In another form of the attack, autoenable of special remotes is not needed. ++Instead, the attacker provides a special remote (eg a S3 bucket) along with ++their git-annex repository. The attacker waits for the victim to clone the ++repository and manually enable and use their special remote. Then they ++update the configuration of the special remote, retargeting it to access ++the victim's own special remote (eg the victim's private S3 bucket). When ++the victim runs a command such as `git-annex sync --content`, their private ++files are retrieved from their special remote and sent to the attacker. ++ ++A third form of the attack only requires that the attacker knows the ++location of the victim's private git repository. The attacker provides a ++git-annex repository that the victim clones. When git-annex is initialized ++in the victim's clone, it autoenables a git remote pointing to the victim's ++private git repository. The first time the victim runs a command such as ++`git-annex sync --content`, the attacker learns what git-annex keys are ++used by annexed files in the private git repository, and adds them to the ++tree. The second time the victim runs such a command, their private files ++are retrieved from their private git repository and sent to the attacker. ++ ++## git-annex security fixes ++ ++`git-annex enableremote` now displays the configuration of a special remote ++to the user and requires that they verify it's safe before it can be used. ++ ++When a repository has an autoenabled special remote, it can be used ++before verification, but git-annex will refuse to send annexed files ++to any remote until the user verifies that the configuration of the ++autoenabled special remote is safe. ++ ++git-annex also installs a git pre-push hook, which blocks git pushes ++when there is an unverified special remote. ++ ++Additionally, changes to the configuration of already enabled ++special remotes, that are received via the git-annex branch, ++will not take effect until the user has verified they are safe. ++ ++After upgrading git-annex, existing special remotes will continue to work ++as before. If a special remote could have been configured by a third party, ++you should verify its configuration using the `git-annex verifyremote` ++command. +--- git-annex-10.20250416.orig/git-annex.cabal ++++ git-annex-10.20250416/git-annex.cabal +@@ -727,6 +727,7 @@ Executable git-annex + Command.P2PStdIO + Command.PostReceive + Command.PreCommit ++ Command.PrePush + Command.Proxy + Command.Pull + Command.Push +@@ -776,6 +777,7 @@ Executable git-annex + Command.VCycle + Command.VFilter + Command.VPop ++ Command.VerifyRemote + Command.Version + Command.Vicfg + Command.View +@@ -912,6 +914,7 @@ Executable git-annex + Logs.UUIDBased + Logs.Unused + Logs.Upgrade ++ Logs.VerifiedRemote + Logs.View + Logs.Web + Messages