diff --git a/README.md b/README.md index 0c40438..a61cfe2 100644 --- a/README.md +++ b/README.md @@ -299,17 +299,42 @@ a request URL includes components after the path to an executable executable (e.g. `/var/gemini/cgi-bin/scripty.py`) while the variable `PATH_INFO` will contain the remainder (e.g. `foo/bar/baz`). -It is very important to be aware that programs written in Go are -*unable* to reliably change their UID once started, due to how -goroutines are implemented on unix systems. As an unavoidable -consequence of this, CGI processes started by Molly Brown are run as -the same user as the server process. This means CGI processes -necessarily have read and write access to the server logs and to the -TLS private key. There is no way to work around this. As such you -must be extremely careful about only running trustworthy CGI -applications, ideally only applications you have carefully written -yourself. Allowing untrusted users to upload arbitrary executable -files into a CGI path is a serious security vulnerability. +Molly Brown itself tries very hard to avoid being tricked into serving +content that isn't supposed to be served, but it is completely unable +to impose any control over what CGI processes can or can't go after +they are started! Where possible, Molly Brown will use the operating +system's security features to reduce risk, but it is your +responsibility to understand what it can and cannot do and weigh the +risks accordingly: + +When compiled on GNU/Linux with Go version 1.16 or later, or on any +other unix operating system with any version of Go, Molly Brown will +use the setuid() system call as follows. When the compiled +`molly-brown` executable has its SETUID bit set, so that it starts +with the privileges of the user who owns the binary, it will change +the effective UID back to the real UID before it begins accepting +network connections. This way, config files, log files and TLS keys +can be set readable by the user who owns the binary, but not readable +by the user who runs the binary. CGI processes will then be unable to +read any of those sensitive files. If the binary is not SETUID but is +run by the superuser/root, then Molly will change its UID to that of +the `nobody` user before accepting network connections, so CGI +processes will again not be able to read sensitive files. + +When compiled on GNU/Linux with Go versions 1.15 or earlier, Molly +Brown is completley unable to reliably change its UID due to the way +early implementations of goroutines interacted with the setuid() +system call. In this situation, Molly Brown will refuse to run as +superuser/root. It will run as any other user, but CGI processes will +necessary run as the same user as the server and so unavoidably will +have access to sensitive files. You should proceed with extreme +caution and only use carefully vetted CGI programs (or upgrade Go). + +Molly Brown will compile on non-unix operating systems and is known to +run on Plan9, for example, but no special security measures are taken +on these non-unix platforms. It is your responsibility to understand +the risks. If you are aware of security measures for these systems +which can be implemented in Go, patches are extremely welcome. SCGI applications must be started separately (i.e. Molly Brown expects them to already be running and will not attempt to start them itself), diff --git a/security.go b/security.go index 1ae9423..5dd86bd 100644 --- a/security.go +++ b/security.go @@ -1,4 +1,4 @@ -// +build !openbsd +// +build js nacl plan9 windows package main diff --git a/security_dropprivs.go b/security_dropprivs.go new file mode 100644 index 0000000..59ad693 --- /dev/null +++ b/security_dropprivs.go @@ -0,0 +1,45 @@ +// +build linux,go1.16 aix darwin dragonfly freebsd illumos netbsd openbsd solaris + +package main + +import ( + "log" + "os" + "os/user" + "strconv" + "syscall" +) + +func DropPrivs(config Config, errorLog *log.Logger) { + + // Get our real and effective UIDs + uid := os.Getuid() + euid := os.Geteuid() + + // If these are equal and non-zero, there's nothing to do + if uid == euid && uid != 0 { + return + } + + // If our real UID is root, we need to lookup the nobody UID + if uid == 0 { + user, err := user.Lookup("nobody") + if err != nil { + errorLog.Println("Could not lookup UID for user " + "nobody" + ": " + err.Error()) + log.Fatal(err) + } + uid, err = strconv.Atoi(user.Uid) + if err != nil { + errorLog.Println("Could not lookup UID fr user " + "nobody" + ": " + err.Error()) + log.Fatal(err) + } + } + + // Drop priveleges + err := syscall.Setuid(uid) + if err != nil { + errorLog.Println("Could not setuid to " + strconv.Itoa(uid) + ": " + err.Error()) + log.Fatal(err) + } + +} diff --git a/security_oldgolinux.go b/security_oldgolinux.go new file mode 100644 index 0000000..e5f1675 --- /dev/null +++ b/security_oldgolinux.go @@ -0,0 +1,21 @@ +// +build linux,!go1.16 + +package main + +import ( + "log" + "os" +) + +func enableSecurityRestrictions(config Config, errorLog *log.Logger) { + + // Prior to Go 1.6, setuid did not work reliably on Linux + // So, absolutely refuse to run as root + uid := os.Getuid() + euid := os.Geteuid() + if uid == 0 || euid == 0 { + setuid_err := "Refusing to run with root privileges when setuid() will not work!" + errorLog.Println(setuid_err) + log.Fatal(setuid_err) + } +} diff --git a/security_openbsd.go b/security_openbsd.go index bfcd1ce..8afcabe 100644 --- a/security_openbsd.go +++ b/security_openbsd.go @@ -13,6 +13,9 @@ import ( // and should pledge their own restrictions and unveil their own files. func enableSecurityRestrictions(config Config, errorLog *log.Logger) { + // Setuid to an unprivileged user + DropPrivs(config, errorLog) + // Unveil the configured document base as readable. log.Println("Unveiling \"" + config.DocBase + "\" as readable.") err := unix.Unveil(config.DocBase, "r") diff --git a/security_other_unix.go b/security_other_unix.go new file mode 100644 index 0000000..0026358 --- /dev/null +++ b/security_other_unix.go @@ -0,0 +1,13 @@ +// +build linux,go1.16 aix darwin dragonfly freebsd illumos netbsd solaris + +package main + +import ( + "log" +) + +func enableSecurityRestrictions(config Config, errorLog *log.Logger) { + + // Setuid to an unprivileged user + DropPrivs(config, errorLog) +}