Skip to content

Commit 9c10840

Browse files
committed
local socket: tighten access rights
1 parent d85b45b commit 9c10840

9 files changed

Lines changed: 219 additions & 9 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!--
2+
A new scriv changelog fragment.
3+
4+
Uncomment the section that is right (remove the HTML comment wrapper).
5+
For top level release notes, leave all the headers commented out.
6+
-->
7+
8+
### Breaking
9+
10+
- Added `Interfaces.diNtcConfigureSocketFileDescriptor`. It receives the local
11+
socket file descriptor. It is called before `bind.
12+
- Added `Interfaces.diNtcConfigureSocketFile`. It recieves the file path of
13+
the local socket. It is called after `bind` and before `listen`.
14+
- Added two `DiffusionTracer` constructors: `ConfiguredLocalSocket` (fired
15+
after the local socket's permissions are tightened) and
16+
`InsecureLocalSocketDirectory` (warning when the parent directory of the
17+
local socket has group or other write permission).
18+
- `mkInterfaces`'s tracer type is now specialised to `LocalAddress`
19+
(`Tracer IO (DiffusionTracer ntnAddr LocalAddress)`).
20+
21+
### Non-Breaking
22+
23+
- Tighten access rights on the local (Node-to-Client) socket: on POSIX the
24+
socket file is restricted to mode `0600` (owner read/write) via both
25+
`fchmod` on the socket file descriptor and `chmod` on the bound path.
26+
No-op on Windows (named pipes use ACLs) and on WASM (WASI does not
27+
implement the POSIX permission model meaningfully).
28+
- Trace a warning at start-up if the parent directory of the local-socket
29+
path has `group` or `other` write permission, since a `0600` socket
30+
inside such a directory remains vulnerable to manipulation by another
31+
local user with write access to that directory.
32+
33+
<!--
34+
### Patch
35+
36+
- A bullet item for the Patch category.
37+
38+
-->

ouroboros-network/framework/lib/Ouroboros/Network/Snocket.hs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
{-# LANGUAGE RankNTypes #-}
1111
{-# LANGUAGE ScopedTypeVariables #-}
1212
{-# LANGUAGE StandaloneDeriving #-}
13+
{-# LANGUAGE TypeApplications #-}
1314
{-# LANGUAGE TypeSynonymInstances #-}
1415

1516
#if !defined(mingw32_HOST_OS)
@@ -46,6 +47,8 @@ module Ouroboros.Network.Snocket
4647
, FileDescriptor
4748
, socketFileDescriptor
4849
, localSocketFileDescriptor
50+
, configureLocalSocketFileDescriptor
51+
, configureLocalSocketFile
4952
-- ** for testing
5053
, invalidFileDescriptor
5154
-- * Re-exports
@@ -56,12 +59,12 @@ import Control.DeepSeq (NFData (..))
5659
import Control.Exception
5760
import Data.Bifoldable (Bifoldable (..))
5861
import Data.Bifunctor (Bifunctor (..))
62+
import Data.Bits
5963
import Data.Hashable
6064
import Data.Word
6165
import GHC.Generics (Generic)
6266
import Quiet (Quiet (..))
6367
#if defined(mingw32_HOST_OS)
64-
import Data.Bits
6568
import Foreign.Ptr (IntPtr (..), ptrToIntPtr)
6669
import System.Win32 qualified as Win32
6770
import System.Win32.Async qualified as Win32.Async
@@ -72,6 +75,10 @@ import "Win32" System.Win32.NamedPipes qualified as Win32.NamedPipes
7275
-- earlier version of Win32-network comes with `NamedPipes` APIs
7376
import "Win32-network" System.Win32.NamedPipes qualified as Win32.NamedPipes
7477
#endif
78+
#elif !defined(wasm32_HOST_ARCH)
79+
import Foreign.C (CInt)
80+
import System.Posix.Files qualified as Unix
81+
import System.Posix.Types qualified as Unix
7582
#endif
7683

7784
import NoThunks.Class
@@ -632,6 +639,49 @@ localSocketFileDescriptor =
632639
localSocketFileDescriptor = socketFileDescriptor . getLocalHandle
633640
#endif
634641

642+
-- | Restrict the local (Node-to-Client) socket so that only the owning user or
643+
-- one in its group can connect.
644+
--
645+
-- On POSIX this sets mode @0600@ on the AF_UNIX socket. Only @fchmod@ on the
646+
-- socket file descriptor is called. On `Linux` this will also set file mode on
647+
-- the socket file once its created when `bind` is called.
648+
--
649+
-- On Windows this is a no-op: the local handle is a named pipe whose ACL is
650+
-- set at creation time and is not configurable via POSIX file modes.
651+
--
652+
-- On WASM this is a no-op: WASI does not implement the POSIX permission
653+
-- model in any meaningful way.
654+
--
655+
-- Must be called between `Snocket.open` and 'Snocket.bind'.
656+
configureLocalSocketFileDescriptor :: LocalSocket -> IO ()
657+
#if defined(mingw32_HOST_OS) || defined(wasm32_HOST_ARCH)
658+
configureLocalSocketFileDescriptor _ = return ()
659+
#else
660+
configureLocalSocketFileDescriptor sock = do
661+
FileDescriptor fd <- localSocketFileDescriptor sock
662+
let mode = Unix.ownerReadMode .|. Unix.ownerWriteMode
663+
Unix.setFdMode (Unix.Fd (fromIntegral @Int @CInt fd)) mode
664+
#endif
665+
666+
-- | Restrict the local (Node-to-Client) socket so that only the owning user or
667+
-- one in its group can connect.
668+
--
669+
-- This must be called between `bind` and `listen`. On macOS, this is the only
670+
-- way to set file permissions on the socket.
671+
--
672+
-- On Windows and WASM this is a no-op (see
673+
-- `configureLocalSocketFileDescriptor`).
674+
--
675+
configureLocalSocketFile :: LocalAddress -> IO ()
676+
#if defined(mingw32_HOST_OS) || defined(wasm32_HOST_ARCH)
677+
configureLocalSocketFile _ = return ()
678+
#else
679+
configureLocalSocketFile LocalAddress { getFilePath = path } = do
680+
let mode = Unix.ownerReadMode .|. Unix.ownerWriteMode
681+
Unix.setFileMode path mode
682+
#endif
683+
684+
635685
-- | invalidFileDescriptor - when we need something for testing/simulation
636686
invalidFileDescriptor :: FileDescriptor
637687
invalidFileDescriptor = FileDescriptor (-1)

ouroboros-network/lib/Ouroboros/Network/Diffusion.hs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import Control.Concurrent.Class.MonadMVar (MonadMVar)
2525
import Control.Concurrent.Class.MonadSTM.Strict
2626
import Control.DeepSeq (NFData)
2727
import Control.Exception (IOException)
28+
#if !defined(mingw32_HOST_OS) && !defined(wasm32_HOST_ARCH)
29+
import Control.Monad (when)
30+
#endif
2831
import Control.Monad.Class.MonadAsync (Async, MonadAsync)
2932
import Control.Monad.Class.MonadAsync qualified as Async
3033
import Control.Monad.Class.MonadFork
@@ -33,6 +36,10 @@ import Control.Monad.Class.MonadTime.SI
3336
import Control.Monad.Class.MonadTimer.SI
3437
import Control.Monad.Fix (MonadFix)
3538
import Control.Tracer (Tracer, contramap, nullTracer, traceWith)
39+
#if !defined(mingw32_HOST_OS) && !defined(wasm32_HOST_ARCH)
40+
import Data.Bits ((.|.))
41+
import System.Posix.Files qualified as Unix
42+
#endif
3643
import Data.ByteString.Lazy (ByteString)
3744
import Data.Hashable (Hashable)
3845
import Data.IP qualified as IP
@@ -77,8 +84,10 @@ import Ouroboros.Network.PeerSharing (PeerSharingRegistry (..))
7784
import Ouroboros.Network.Protocol.Handshake
7885
import Ouroboros.Network.RethrowPolicy
7986
import Ouroboros.Network.Server qualified as Server
80-
import Ouroboros.Network.Snocket (LocalAddress, LocalSocket (..), RemoteAddress,
81-
localSocketFileDescriptor, makeLocalBearer, makeSocketBearer')
87+
import Ouroboros.Network.Snocket (LocalAddress (..), LocalSocket (..),
88+
RemoteAddress, configureLocalSocketFile,
89+
configureLocalSocketFileDescriptor, localSocketFileDescriptor,
90+
makeLocalBearer, makeSocketBearer')
8291
import Ouroboros.Network.Snocket qualified as Snocket
8392
import Ouroboros.Network.Socket (configureSocket, configureSystemdSocket)
8493
import Ouroboros.Network.Util (PrettyShow (..))
@@ -164,6 +173,8 @@ runM Interfaces
164173
, diNtcSnocket
165174
, diNtcBearer
166175
, diNtcGetFileDescriptor
176+
, diNtcConfigureSocketFileDescriptor
177+
, diNtcConfigureSocketFile
167178
, diRng
168179
, diDnsActions
169180
, diConnStateIdSupply
@@ -318,7 +329,10 @@ runM Interfaces
318329
mkLocalThread :: ThreadId m -> Either ntcFd ntcAddr -> m Void
319330
mkLocalThread mainThreadId localAddr = do
320331
labelThisThread "diffusion-local"
321-
withLocalSocket tracer diNtcGetFileDescriptor diNtcSnocket localAddr
332+
withLocalSocket tracer diNtcGetFileDescriptor
333+
diNtcConfigureSocketFileDescriptor
334+
diNtcConfigureSocketFile
335+
diNtcSnocket localAddr
322336
$ \localSocket -> do
323337
localInbInfoChannel <- newInformationChannel
324338

@@ -912,7 +926,7 @@ run extraParams tracers args apps = do
912926
--
913927

914928
mkInterfaces :: IOManager
915-
-> Tracer IO (DiffusionTracer ntnAddr ntcAddr)
929+
-> Tracer IO (DiffusionTracer ntnAddr LocalAddress)
916930
-> DiffTime
917931
-> IO (Interfaces Socket
918932
RemoteAddress
@@ -921,7 +935,6 @@ mkInterfaces :: IOManager
921935
Resolver
922936
IO)
923937
mkInterfaces iocp tracer egressPollInterval = do
924-
925938
diRng <- newStdGen
926939
diConnStateIdSupply <- atomically $ CM.newConnStateIdSupply Proxy
927940

@@ -941,11 +954,45 @@ mkInterfaces iocp tracer egressPollInterval = do
941954
diNtcSnocket = Snocket.localSnocket iocp,
942955
diNtcBearer = makeLocalBearer,
943956
diNtcGetFileDescriptor = localSocketFileDescriptor,
957+
diNtcConfigureSocketFileDescriptor = configureLocalSocketFileDescriptor,
958+
diNtcConfigureSocketFile = \addr -> do
959+
configureLocalSocketFile addr
960+
checkLocalSocketDirectory tracer addr,
944961
diDnsActions = RootPeersDNS.ioDNSActions,
945962
diRng,
946963
diConnStateIdSupply
947964
}
948965

966+
-- | Trace a warning if the parent directory of a local-socket path has
967+
-- @group@ or @other@ write permission. A @0600@ socket inside such a
968+
-- directory is still vulnerable to manipulation by another local user
969+
-- with write access to that directory.
970+
--
971+
-- No-op on Windows (named pipes do not live in a POSIX-permissioned
972+
-- directory) and on WASM (WASI does not implement the POSIX permission
973+
-- model in any meaningful way).
974+
checkLocalSocketDirectory
975+
:: Tracer IO (DiffusionTracer ntnAddr LocalAddress)
976+
-> LocalAddress
977+
-> IO ()
978+
#if defined(mingw32_HOST_OS) || defined(wasm32_HOST_ARCH)
979+
checkLocalSocketDirectory _ _ = return ()
980+
#else
981+
checkLocalSocketDirectory tracer addr@(LocalAddress path) = do
982+
let dir = parentDirectory path
983+
st <- Unix.getFileStatus dir
984+
let mode = Unix.fileMode st
985+
writeMask = Unix.groupWriteMode .|. Unix.otherWriteMode
986+
when (Unix.intersectFileModes mode writeMask /= Unix.nullFileMode) $
987+
traceWith tracer (InsecureLocalSocketDirectory addr (fromIntegral mode))
988+
where
989+
parentDirectory :: FilePath -> FilePath
990+
parentDirectory p = case break (== '/') (reverse p) of
991+
(_, '/' : revParent) | not (null revParent) -> reverse revParent
992+
(_, '/' : _) -> "/"
993+
_ -> "."
994+
#endif
995+
949996
--
950997
-- Data flow
951998
--

ouroboros-network/lib/Ouroboros/Network/Diffusion/Types.hs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import Data.Maybe.Strict
6161
import Data.Set (Set)
6262
import Data.Typeable (Typeable)
6363
import Data.Void (Void)
64+
import Data.Word (Word32)
6465
import System.Random (StdGen)
6566

6667
import Network.Mux qualified as Mx
@@ -109,8 +110,15 @@ data DiffusionTracer ntnAddr ntcAddr
109110
| CreateSystemdSocketForSnocketPath ntcAddr
110111
| CreatedLocalSocket ntcAddr
111112
| ConfiguringLocalSocket ntcAddr FileDescriptor
113+
| ConfiguredLocalSocket ntcAddr FileDescriptor
112114
| ListeningLocalSocket ntcAddr FileDescriptor
113115
| LocalSocketUp ntcAddr FileDescriptor
116+
-- | The parent directory of the local socket has @group@ or @other@ write
117+
-- bits set. This is a warning, not a failure: the socket itself is
118+
-- mode @0600@, but the parent directory's permissions may still allow
119+
-- another local user to rename or unlink the socket file. The 'Word32'
120+
-- is the parent directory's POSIX mode.
121+
| InsecureLocalSocketDirectory ntcAddr Word32
114122
-- Rename as 'CreateServerSocket'
115123
| CreatingServerSocket ntnAddr
116124
| ConfiguringServerSocket ntnAddr
@@ -767,6 +775,16 @@ data Interfaces ntnFd ntnAddr ntcFd ntcAddr
767775
diNtcGetFileDescriptor
768776
:: ntcFd -> m FileDescriptor,
769777

778+
-- | configure node-to-client socket file descriptor (called between
779+
-- `socket` and `bind`).
780+
diNtcConfigureSocketFileDescriptor
781+
:: ntcFd -> m (),
782+
783+
-- | configure node-to-client socket file (called between
784+
-- `bind` and `listen`).
785+
diNtcConfigureSocketFile
786+
:: ntcAddr -> m (),
787+
770788
-- | diffusion pseudo random generator. It is split between various
771789
-- components that need randomness, e.g. inbound governor, peer
772790
-- selection, policies, etc.

ouroboros-network/lib/Ouroboros/Network/Diffusion/Utils.hs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,20 @@ withLocalSocket :: forall ntnAddr ntcFd ntcAddr m a.
8181
)
8282
=> Tracer m (DiffusionTracer ntnAddr ntcAddr)
8383
-> (ntcFd -> m FileDescriptor)
84+
-> (ntcFd -> m ())
85+
-- ^ configure the local socket file descriptor (e.g. set
86+
-- @0600@ permissions on POSIX)
87+
-> (ntcAddr -> m ())
88+
-- ^ configure the local socket file (e.g. set @0600@
89+
-- permissions on POSIX)
8490
-> Snocket m ntcFd ntcAddr
8591
-> Either ntcFd ntcAddr
8692
-> (ntcFd -> m a)
8793
-> m a
88-
withLocalSocket tracer getFileDescriptor sn localAddress k =
94+
withLocalSocket tracer getFileDescriptor
95+
configureSocketFileDescriptor
96+
configureSocketFile
97+
sn localAddress k =
8998
bracket
9099
(
91100
case localAddress of
@@ -116,7 +125,11 @@ withLocalSocket tracer getFileDescriptor sn localAddress k =
116125
Right (sd, addr) -> do
117126
traceWith tracer . ConfiguringLocalSocket addr
118127
=<< getFileDescriptor sd
128+
configureSocketFileDescriptor sd
119129
Snocket.bind sn sd addr
130+
configureSocketFile addr
131+
traceWith tracer . ConfiguredLocalSocket addr
132+
=<< getFileDescriptor sd
120133
traceWith tracer . ListeningLocalSocket addr
121134
=<< getFileDescriptor sd
122135
Snocket.listen sn sd

ouroboros-network/orphan-instances/Ouroboros/Network/OrphanInstances.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,11 @@ instance (ToJSON localAddress, ToJSON remoteAddress) => ToJSON (DiffusionTracer
704704
, "address" .= localAddress
705705
, "socket" .= String (pack (show socket))
706706
]
707+
toJSON (ConfiguredLocalSocket localAddress socket) = object
708+
[ "kind" .= String "ConfiguredLocalSocket"
709+
, "address" .= localAddress
710+
, "socket" .= String (pack (show socket))
711+
]
707712
toJSON (ListeningLocalSocket localAddress socket) = object
708713
[ "kind" .= String "ListeningLocalSocket"
709714
, "address" .= localAddress
@@ -714,6 +719,11 @@ instance (ToJSON localAddress, ToJSON remoteAddress) => ToJSON (DiffusionTracer
714719
, "address" .= localAddress
715720
, "socket" .= String (pack (show fd))
716721
]
722+
toJSON (InsecureLocalSocketDirectory localAddress mode) = object
723+
[ "kind" .= String "InsecureLocalSocketDirectory"
724+
, "address" .= localAddress
725+
, "mode" .= String (pack (show mode))
726+
]
717727
toJSON (CreatingServerSocket sockAddr) = object
718728
[ "kind" .= String "CreatingServerSocket"
719729
, "address" .= sockAddr

ouroboros-network/ouroboros-network.cabal

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,8 @@ library
329329

330330
if !os(windows)
331331
build-depends:
332-
directory
332+
directory,
333+
unix,
333334

334335
library framework
335336
import: ghc-options
@@ -400,6 +401,9 @@ library framework
400401

401402
if os(windows)
402403
build-depends: Win32 >=2.5.4.1 && <3.0
404+
405+
if !os(windows)
406+
build-depends: unix
403407
hs-source-dirs: framework
404408

405409
library tests-lib

ouroboros-network/tests/lib/Test/Ouroboros/Network/Diffusion/Node.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,14 @@ run blockGeneratorArgs ni na
273273
, Diffusion.diWithBuffer = \f -> f Nothing
274274
, Diffusion.diNtnConfigureSocket = \_ _ -> return ()
275275
, Diffusion.diNtnConfigureSystemdSocket
276-
= \_ _ -> return ()
276+
= \_ _ -> return ()
277277
, Diffusion.diNtnAddressType = ntnAddressType
278278
, Diffusion.diNtnToPeerAddr = \a b -> TestAddress (Node.IPAddr a b)
279279
, Diffusion.diNtcSnocket = iNtcSnocket ni
280280
, Diffusion.diNtcBearer = iNtcBearer ni
281281
, Diffusion.diNtcGetFileDescriptor = \_ -> pure invalidFileDescriptor
282+
, Diffusion.diNtcConfigureSocketFileDescriptor = \_ -> pure ()
283+
, Diffusion.diNtcConfigureSocketFile = \_ -> pure ()
282284
, Diffusion.diRng = diffStgGen
283285
, Diffusion.diDnsActions = \tracer lookupType toPeerAddr ->
284286
mockDNSActions

0 commit comments

Comments
 (0)