cg-net: listen on this machine only by default

Finding 1 of the 2026-07-29 security review: hosting bound 0.0.0.0 --
every interface -- with no password and no encryption, so anyone who
could reach the machine's port was handed a seat and the game state.

New defcustom cg-net-host-address defaults to 127.0.0.1; the wide-open
value remains available and its docstring says exactly what choosing it
means.  This converts "anyone who can reach you" into "someone you
deliberately let in", and bounds Finding 4's unauthenticated
memory-exhaustion route to local callers along the way.

Test cgt-net-host-loopback-default checks both the default value and
the actual bound address (process-contact :local).  Against the
previous code it fails with (void-variable cg-net-host-address); the
old bind, captured before the patch: [0 0 0 0 PORT].
This commit is contained in:
Claude 2026-08-03 20:31:58 -05:00 committed by Corwin Brust
parent 9d3ec08d3c
commit f8873fb8fa
2 changed files with 39 additions and 2 deletions

View file

@ -60,6 +60,23 @@
"Default TCP port used to host or join a game." "Default TCP port used to host or join a game."
:type 'integer :group 'cg-net) :type 'integer :group 'cg-net)
(defcustom cg-net-host-address "127.0.0.1"
"Address the host's listening socket binds when hosting a game.
The default, \"127.0.0.1\", accepts connections only from this
machine; remote players reach it through a tunnel they were
deliberately given (for example ssh port forwarding), which also
encrypts the traffic in transit.
Setting this to \"0.0.0.0\" listens on every network interface, which
means anyone able to reach this machine's port can take a seat: there
is no password and no encryption on the wire. That can be a
reasonable choice on a trusted LAN, but it is a choice -- make it
deliberately."
:type '(choice (const :tag "This machine only (recommended)" "127.0.0.1")
(const :tag "Every interface (anyone who can reach you)" "0.0.0.0")
(string :tag "A specific interface address"))
:group 'cg-net)
(defvar cg-net-state-functions nil (defvar cg-net-state-functions nil
"Abnormal hook run on a client after the game state is updated. "Abnormal hook run on a client after the game state is updated.
Each function is called with the client's game object.") Each function is called with the client's game object.")
@ -157,11 +174,12 @@ properties stripped (`cg-net--scrub') before HANDLER sees them."
(and cg-net--host (process-live-p (cg-net-host-server cg-net--host)))) (and cg-net--host (process-live-p (cg-net-host-server cg-net--host))))
(defun cg-net-host-start (game &optional port) (defun cg-net-host-start (game &optional port)
"Begin hosting GAME on PORT (default `cg-net-port'). Return the server process." "Begin hosting GAME on PORT (default `cg-net-port'). Return the server process.
The socket binds `cg-net-host-address' -- by default, this machine only."
(let* ((port (or port cg-net-port)) (let* ((port (or port cg-net-port))
(server (make-network-process (server (make-network-process
:name "cg-host" :server t :service port :name "cg-host" :server t :service port
:host "0.0.0.0" :family 'ipv4 :coding 'utf-8 :host cg-net-host-address :family 'ipv4 :coding 'utf-8
:log #'cg-net--host-accept))) :log #'cg-net--host-accept)))
(setq cg-net--host (cg-net--host-make :server server :game game)) (setq cg-net--host (cg-net--host-make :server server :game game))
server)) server))

View file

@ -104,6 +104,25 @@ host must reach the client bare."
(cg-net-disconnect) (cg-net-disconnect)
(cg-net-host-stop))))) (cg-net-host-stop)))))
(ert-deftest cgt-net-host-loopback-default ()
"Hosting binds to this machine only unless deliberately widened.
Finding 1 of the 2026-07-29 review: the default of
`cg-net-host-address' is loopback, and the listening socket really
binds it -- wider exposure is a setting the user turns on, not a
silent default."
(should (equal "127.0.0.1" (default-value 'cg-net-host-address)))
(condition-case _
(delete-process
(make-network-process :name "cgt-probe4" :server t :service 0
:host "127.0.0.1" :family 'ipv4))
(error (ert-skip "TCP not available")))
(let* ((hgame (make-instance 'cgt-net-game :env (list :counter 0)))
(srv (cg-net-host-start hgame 0)))
(unwind-protect
(should (equal [127 0 0 1]
(substring (process-contact srv :local) 0 4)))
(cg-net-host-stop))))
;;;; Gaps ;;;; Gaps
(ert-deftest cgt-gaps-deal () (ert-deftest cgt-gaps-deal ()