Convert CGIPath handling from regexs to prefixes.

This commit is contained in:
Solderpunk 2020-07-01 14:10:20 +02:00
parent cc5410494e
commit 4ae154faed
4 changed files with 52 additions and 37 deletions

View File

@ -224,6 +224,18 @@ directory listing:
Molly Brown supports dynamically generated content using an adaptation Molly Brown supports dynamically generated content using an adaptation
of the CGI standard, and also the SCGI standard. of the CGI standard, and also the SCGI standard.
The `stdout` of CGI processes will be sent verbatim as the response to
the client, and CGI applications are responsible for generating their
own response headers. CGI processes must terminate naturally within
10 seconds of being spawned to avoid being killed. Details about the
request are available to CGI applications through environment
variables, generally following RFC 3875. In particular, note that if
a request URL includes components after the path to an executable
(e.g. `cgi-bin/script.py/foo/bar/baz`) then the environment variable
`SCRIPT_PATH` will contain the part of the URL path mapping to the
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 It is very important to be aware that programs written in Go are
*unable* to reliably change their UID once started, due to how *unable* to reliably change their UID once started, due to how
goroutines are implemented on unix systems. As an unavoidable goroutines are implemented on unix systems. As an unavoidable
@ -236,18 +248,23 @@ applications, ideally only applications you have carefully written
yourself. Allowing untrusted users to upload arbitrary executable yourself. Allowing untrusted users to upload arbitrary executable
files into a CGI path is a serious security vulnerability. files into a CGI path is a serious security vulnerability.
SCGI applications must be started separately, and as such can run e.g. SCGI applications must be started separately (i.e. Molly Brown expects
as their own user and/or chrooted into their own filesystem, and as them to already be running and will not attempt to start them itself),
such are less of a security issue. and as such they can run e.g. as their own user and/or chrooted into
their own filesystem, meaning that they are less of a security threat
in addition to avoiding the overhead of process startup, database
connection etc. on each request.
* `CGIPaths`: A list of path regexs. Any request which maps to a * `CGIPaths`: A list of filesystem paths, within which
world-executable file contained in a directory which matches one of world-executable files will be run as CGI processes. The paths act
the regexs in this list will be executed and its standard output as prefixes, i.e. if `/var/gemini/cgi-bin` is listed then
will be sent as the response to the client. CGI applications are `/var/gemini/cgi-bin/script.py` and
responsible for generating their own response headers, and must `/var/gemini/cgi-bin/subdir/subsubdir/script.py` will both be run.
terminate within 10 seconds of being spawned to avoid being killed. The paths may include basic wildcard characters, where `?` matches a
Details about the request are available to CGI applications through single non-separator character and `*` matches a sequence of them -
environment variables. if wildcards are used, the path should *not* end in a trailing slash
- this appears to be a peculiarity of the Go standard library's
`filepath.Glob` function.
* `SCGIPaths`: In this section of the config file, keys are path * `SCGIPaths`: In this section of the config file, keys are path
regexs and values are paths to unix domain sockets. Any request regexs and values are paths to unix domain sockets. Any request
whose path matches one of the regexs will cause an SCGI request to whose path matches one of the regexs will cause an SCGI request to

View File

@ -9,40 +9,30 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
func handleCGI(config Config, path string, cgiPath string, URL *url.URL, log *LogEntry, errorLog chan string, conn net.Conn) { func handleCGI(config Config, path string, cgiPath string, URL *url.URL, log *LogEntry, errorLog chan string, conn net.Conn) {
// Attempt to find the shortest leading part of path which maps to an executable file while still matching cgiPath // Find the shortest leading part of path which maps to an executable file.
// If we find such, call it script_path, and everything after it path_info // Call this part scriptPath, and everything after it pathInfo.
components := strings.Split(path, "/") components := strings.Split(path, "/")
script_path := "" scriptPath := ""
path_info := "" pathInfo := ""
matched := false matched := false
for i := 0; i <= len(components); i++ { for i := 0; i <= len(components); i++ {
script_path = strings.Join(components[0:i], "/") scriptPath = strings.Join(components[0:i], "/")
path_info = strings.Join(components[i:], "/") pathInfo = strings.Join(components[i:], "/")
if !strings.HasPrefix(script_path, config.DocBase) { if !strings.HasPrefix(scriptPath, cgiPath) {
continue continue
} }
inCGIPath, err := regexp.Match(cgiPath, []byte(path)) info, err := os.Stat(scriptPath)
if err != nil { if err != nil {
break break
} } else if info.IsDir() {
if !inCGIPath {
continue continue
} } else if info.Mode().Perm()&0111 == 0111 {
info, err := os.Stat(script_path)
if err != nil {
break
}
if info.IsDir() {
continue
}
if info.Mode().Perm()&0111 == 0111 {
matched = true matched = true
break break
} }
@ -54,12 +44,12 @@ func handleCGI(config Config, path string, cgiPath string, URL *url.URL, log *Lo
} }
// Prepare environment variables // Prepare environment variables
vars := prepareCGIVariables(config, URL, conn, script_path, path_info) vars := prepareCGIVariables(config, URL, conn, scriptPath, pathInfo)
// Spawn process // Spawn process
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, script_path) cmd := exec.CommandContext(ctx, scriptPath)
cmd.Env = []string{} cmd.Env = []string{}
for key, value := range vars { for key, value := range vars {
cmd.Env = append(cmd.Env, key+"="+value) cmd.Env = append(cmd.Env, key+"="+value)

View File

@ -21,8 +21,8 @@
## Dynamic content ## Dynamic content
# #
#CGIPaths = [ #CGIPaths = [
# "^/var/gemini/cgi-bin/", # "/var/gemini/cgi-bin",
# "^/var/gemini/users/trusted-user/cgi-bin/", # "/var/gemini/users/*/cgi-bin/", # Unsafe!
#] #]
# #
#[SCGIPaths] #[SCGIPaths]

View File

@ -91,9 +91,17 @@ func handleGeminiRequest(conn net.Conn, config Config, accessLogEntries chan Log
} }
// Check whether this URL is in a configured CGI path // Check whether this URL is in a configured CGI path
var cgiPaths []string
for _, cgiPath := range config.CGIPaths { for _, cgiPath := range config.CGIPaths {
inCGIPath, err := regexp.Match(cgiPath, []byte(path)) expandedPaths, err := filepath.Glob(cgiPath)
if err == nil && inCGIPath { if err != nil {
errorLogEntries <- "Error expanding CGI path glob " + cgiPath + ": " + err.Error()
continue
}
cgiPaths = append(cgiPaths, expandedPaths...)
}
for _, cgiPath := range cgiPaths {
if strings.HasPrefix(path, cgiPath) {
handleCGI(config, path, cgiPath, URL, &log, errorLogEntries, conn) handleCGI(config, path, cgiPath, URL, &log, errorLogEntries, conn)
if log.Status != 0 { if log.Status != 0 {
return return