mirror of
https://git.savannah.gnu.org/git/guile.git
synced 2025-04-30 20:00:19 +02:00
Reported by Christopher Allan Webber <cwebber@dustycloud.org> Co-authored-by: Ludovic Courtès <ludo@gnu.org> This commit adds protection to Guile's REPL servers against HTTP inter-protocol exploitation attacks, a scenario whereby an attacker can, via an HTML page, cause a web browser to send data to TCP servers listening on a loopback interface or private network. See <https://en.wikipedia.org/wiki/Inter-protocol_exploitation> and <https://www.jochentopf.com/hfpa/hfpa.pdf>, The HTML Form Protocol Attack (2001) by Tochen Topf <jochen@remote.org>. Here we add a procedure to 'before-read-hook' that looks for a possible HTTP request-line in the first line of input from the client socket. If present, the socket is drained and closed, and a loud warning is written to stderr (POSIX file descriptor 2). * module/system/repl/server.scm: Add 'maybe-check-for-http-request' to 'before-read-hook' when this module is loaded. (with-temporary-port-encoding, with-saved-port-line+column) (drain-input-and-close, permissive-http-request-line?) (check-for-http-request, guard-against-http-request) (maybe-check-for-http-request): New procedures. (serve-client): Use 'guard-against-http-request'. * module/system/repl/coop-server.scm (start-repl-client): Use 'guard-against-http-request'. * doc/ref/guile-invoke.texi (Command-line Options): In the description of the --listen option, make the security warning more prominent. Mention the new protection added here. Recommend using UNIX domain sockets for REPL servers. "a path to" => "the file name of".
200 lines
7.9 KiB
Scheme
200 lines
7.9 KiB
Scheme
;;; Cooperative REPL server
|
|
|
|
;; Copyright (C) 2014, 2016 Free Software Foundation, Inc.
|
|
|
|
;; This library is free software; you can redistribute it and/or
|
|
;; modify it under the terms of the GNU Lesser General Public
|
|
;; License as published by the Free Software Foundation; either
|
|
;; version 3 of the License, or (at your option) any later version.
|
|
;;
|
|
;; This library is distributed in the hope that it will be useful,
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
;; Lesser General Public License for more details.
|
|
;;
|
|
;; You should have received a copy of the GNU Lesser General Public
|
|
;; License along with this library; if not, write to the Free Software
|
|
;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
;; 02110-1301 USA
|
|
|
|
;;; Code:
|
|
|
|
(define-module (system repl coop-server)
|
|
#:use-module (ice-9 match)
|
|
#:use-module (ice-9 receive)
|
|
#:use-module (ice-9 threads)
|
|
#:use-module (ice-9 q)
|
|
#:use-module (srfi srfi-9)
|
|
#:export (spawn-coop-repl-server
|
|
poll-coop-repl-server))
|
|
|
|
;; Hack to import private bindings from (system repl repl).
|
|
(define-syntax-rule (import-private module sym ...)
|
|
(begin
|
|
(define sym (@@ module sym))
|
|
...))
|
|
(import-private (system repl repl) start-repl* prompting-meta-read)
|
|
(import-private (system repl server)
|
|
run-server* add-open-socket! close-socket!
|
|
make-tcp-server-socket guard-against-http-request)
|
|
|
|
(define-record-type <coop-repl-server>
|
|
(%make-coop-repl-server mutex queue)
|
|
coop-repl-server?
|
|
(mutex coop-repl-server-mutex)
|
|
(queue coop-repl-server-queue))
|
|
|
|
(define (make-coop-repl-server)
|
|
(%make-coop-repl-server (make-mutex) (make-q)))
|
|
|
|
(define (coop-repl-server-eval coop-server opcode . args)
|
|
"Queue a new instruction with the symbolic name OPCODE and an arbitrary
|
|
number of arguments, to be processed the next time COOP-SERVER is polled."
|
|
(with-mutex (coop-repl-server-mutex coop-server)
|
|
(enq! (coop-repl-server-queue coop-server)
|
|
(cons opcode args))))
|
|
|
|
(define-record-type <coop-repl>
|
|
(%make-coop-repl mutex condvar thunk cont)
|
|
coop-repl?
|
|
(mutex coop-repl-mutex)
|
|
(condvar coop-repl-condvar) ; signaled when thunk becomes non-#f
|
|
(thunk coop-repl-read-thunk set-coop-repl-read-thunk!)
|
|
(cont coop-repl-cont set-coop-repl-cont!))
|
|
|
|
(define (make-coop-repl)
|
|
(%make-coop-repl (make-mutex) (make-condition-variable) #f #f))
|
|
|
|
(define (coop-repl-read coop-repl)
|
|
"Read an expression via the thunk stored in COOP-REPL."
|
|
(let ((thunk
|
|
(with-mutex (coop-repl-mutex coop-repl)
|
|
(unless (coop-repl-read-thunk coop-repl)
|
|
(wait-condition-variable (coop-repl-condvar coop-repl)
|
|
(coop-repl-mutex coop-repl)))
|
|
(let ((thunk (coop-repl-read-thunk coop-repl)))
|
|
(unless thunk
|
|
(error "coop-repl-read: condvar signaled, but thunk is #f!"))
|
|
(set-coop-repl-read-thunk! coop-repl #f)
|
|
thunk))))
|
|
(thunk)))
|
|
|
|
(define (store-repl-cont cont coop-repl)
|
|
"Save the partial continuation CONT within COOP-REPL."
|
|
(set-coop-repl-cont! coop-repl
|
|
(lambda (exp)
|
|
(coop-repl-prompt
|
|
(lambda () (cont exp))))))
|
|
|
|
(define (coop-repl-prompt thunk)
|
|
"Apply THUNK within a prompt for cooperative REPLs."
|
|
(call-with-prompt 'coop-repl-prompt thunk store-repl-cont))
|
|
|
|
(define (make-coop-reader coop-repl)
|
|
"Return a new procedure for reading user input from COOP-REPL. The
|
|
generated procedure passes the responsibility of reading input to
|
|
another thread and aborts the cooperative REPL prompt."
|
|
(lambda (repl)
|
|
(let ((read-thunk
|
|
;; Need to preserve the REPL stack and current module across
|
|
;; threads.
|
|
(let ((stack (fluid-ref *repl-stack*))
|
|
(module (current-module)))
|
|
(lambda ()
|
|
(with-fluids ((*repl-stack* stack))
|
|
(set-current-module module)
|
|
(prompting-meta-read repl))))))
|
|
(with-mutex (coop-repl-mutex coop-repl)
|
|
(when (coop-repl-read-thunk coop-repl)
|
|
(error "coop-reader: read-thunk is not #f!"))
|
|
(set-coop-repl-read-thunk! coop-repl read-thunk)
|
|
(signal-condition-variable (coop-repl-condvar coop-repl))))
|
|
(abort-to-prompt 'coop-repl-prompt coop-repl)))
|
|
|
|
(define (reader-loop coop-server coop-repl)
|
|
"Run an unbounded loop that reads an expression for COOP-REPL and
|
|
stores the expression within COOP-SERVER for later evaluation."
|
|
(coop-repl-server-eval coop-server 'eval coop-repl
|
|
(coop-repl-read coop-repl))
|
|
(reader-loop coop-server coop-repl))
|
|
|
|
(define (poll-coop-repl-server coop-server)
|
|
"Poll the cooperative REPL server COOP-SERVER and apply a pending
|
|
operation if there is one, such as evaluating an expression typed at the
|
|
REPL prompt. This procedure must be called from the same thread that
|
|
called spawn-coop-repl-server."
|
|
(let ((op (with-mutex (coop-repl-server-mutex coop-server)
|
|
(let ((queue (coop-repl-server-queue coop-server)))
|
|
(and (not (q-empty? queue))
|
|
(deq! queue))))))
|
|
(when op
|
|
(match op
|
|
(('new-repl client)
|
|
(start-repl-client coop-server client))
|
|
(('eval coop-repl exp)
|
|
((coop-repl-cont coop-repl) exp))))
|
|
*unspecified*))
|
|
|
|
(define (start-coop-repl coop-server)
|
|
"Start a new cooperative REPL process for COOP-SERVER."
|
|
;; Calling stop-server-and-clients! from a REPL will cause an
|
|
;; exception to be thrown when trying to read from the socket that has
|
|
;; been closed, so we catch that here.
|
|
(false-if-exception
|
|
(let ((coop-repl (make-coop-repl)))
|
|
(make-thread reader-loop coop-server coop-repl)
|
|
(start-repl* (current-language) #f (make-coop-reader coop-repl)))))
|
|
|
|
(define (run-coop-repl-server coop-server server-socket)
|
|
"Start the cooperative REPL server for COOP-SERVER using the socket
|
|
SERVER-SOCKET."
|
|
(run-server* server-socket (make-coop-client-proc coop-server)))
|
|
|
|
(define* (spawn-coop-repl-server
|
|
#:optional (server-socket (make-tcp-server-socket)))
|
|
"Create and return a new cooperative REPL server object, and spawn a
|
|
new thread to listen for connections on SERVER-SOCKET. Proper
|
|
functioning of the REPL server requires that poll-coop-repl-server be
|
|
called periodically on the returned server object."
|
|
(let ((coop-server (make-coop-repl-server)))
|
|
(make-thread run-coop-repl-server
|
|
coop-server
|
|
server-socket)
|
|
coop-server))
|
|
|
|
(define (make-coop-client-proc coop-server)
|
|
"Return a new procedure that is used to schedule the creation of a new
|
|
cooperative REPL for COOP-SERVER."
|
|
(lambda (client addr)
|
|
(coop-repl-server-eval coop-server 'new-repl client)))
|
|
|
|
(define (start-repl-client coop-server client)
|
|
"Run a cooperative REPL for COOP-SERVER within a prompt. All input
|
|
and output is sent over the socket CLIENT."
|
|
|
|
;; Add the client to the list of open sockets, with a 'force-close'
|
|
;; procedure that closes the underlying file descriptor. We do it
|
|
;; this way because we cannot close the port itself safely from
|
|
;; another thread.
|
|
(add-open-socket! client (lambda () (close-fdes (fileno client))))
|
|
|
|
(guard-against-http-request client)
|
|
|
|
(with-continuation-barrier
|
|
(lambda ()
|
|
(coop-repl-prompt
|
|
(lambda ()
|
|
(parameterize ((current-input-port client)
|
|
(current-output-port client)
|
|
(current-error-port client)
|
|
(current-warning-port client))
|
|
(with-fluids ((*repl-stack* '()))
|
|
(save-module-excursion
|
|
(lambda ()
|
|
(start-coop-repl coop-server)))))
|
|
|
|
;; This may fail if 'stop-server-and-clients!' is called,
|
|
;; because the 'force-close' procedure above closes the
|
|
;; underlying file descriptor instead of the port itself.
|
|
(false-if-exception
|
|
(close-socket! client)))))))
|