Convert CGIPath handling from regexs to prefixes.
This commit is contained in:
parent
cc5410494e
commit
4ae154faed
4 changed files with 52 additions and 37 deletions
39
README.md
39
README.md
|
@ -224,6 +224,18 @@ directory listing:
|
|||
Molly Brown supports dynamically generated content using an adaptation
|
||||
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
|
||||
*unable* to reliably change their UID once started, due to how
|
||||
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
|
||||
files into a CGI path is a serious security vulnerability.
|
||||
|
||||
SCGI applications must be started separately, and as such can run e.g.
|
||||
as their own user and/or chrooted into their own filesystem, and as
|
||||
such are less of a security issue.
|
||||
SCGI applications must be started separately (i.e. Molly Brown expects
|
||||
them to already be running and will not attempt to start them itself),
|
||||
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
|
||||
world-executable file contained in a directory which matches one of
|
||||
the regexs in this list will be executed and its standard output
|
||||
will be sent as the response to the client. CGI applications are
|
||||
responsible for generating their own response headers, and must
|
||||
terminate within 10 seconds of being spawned to avoid being killed.
|
||||
Details about the request are available to CGI applications through
|
||||
environment variables.
|
||||
* `CGIPaths`: A list of filesystem paths, within which
|
||||
world-executable files will be run as CGI processes. The paths act
|
||||
as prefixes, i.e. if `/var/gemini/cgi-bin` is listed then
|
||||
`/var/gemini/cgi-bin/script.py` and
|
||||
`/var/gemini/cgi-bin/subdir/subsubdir/script.py` will both be run.
|
||||
The paths may include basic wildcard characters, where `?` matches a
|
||||
single non-separator character and `*` matches a sequence of them -
|
||||
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
|
||||
regexs and values are paths to unix domain sockets. Any request
|
||||
whose path matches one of the regexs will cause an SCGI request to
|
||||
|
|
34
dynamic.go
34
dynamic.go
|
@ -9,40 +9,30 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
// If we find such, call it script_path, and everything after it path_info
|
||||
// Find the shortest leading part of path which maps to an executable file.
|
||||
// Call this part scriptPath, and everything after it pathInfo.
|
||||
components := strings.Split(path, "/")
|
||||
script_path := ""
|
||||
path_info := ""
|
||||
scriptPath := ""
|
||||
pathInfo := ""
|
||||
matched := false
|
||||
for i := 0; i <= len(components); i++ {
|
||||
script_path = strings.Join(components[0:i], "/")
|
||||
path_info = strings.Join(components[i:], "/")
|
||||
if !strings.HasPrefix(script_path, config.DocBase) {
|
||||
scriptPath = strings.Join(components[0:i], "/")
|
||||
pathInfo = strings.Join(components[i:], "/")
|
||||
if !strings.HasPrefix(scriptPath, cgiPath) {
|
||||
continue
|
||||
}
|
||||
inCGIPath, err := regexp.Match(cgiPath, []byte(path))
|
||||
info, err := os.Stat(scriptPath)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !inCGIPath {
|
||||
} else if info.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(script_path)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if info.IsDir() {
|
||||
continue
|
||||
}
|
||||
if info.Mode().Perm()&0111 == 0111 {
|
||||
} else if info.Mode().Perm()&0111 == 0111 {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
|
@ -54,12 +44,12 @@ func handleCGI(config Config, path string, cgiPath string, URL *url.URL, log *Lo
|
|||
}
|
||||
|
||||
// Prepare environment variables
|
||||
vars := prepareCGIVariables(config, URL, conn, script_path, path_info)
|
||||
vars := prepareCGIVariables(config, URL, conn, scriptPath, pathInfo)
|
||||
|
||||
// Spawn process
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, script_path)
|
||||
cmd := exec.CommandContext(ctx, scriptPath)
|
||||
cmd.Env = []string{}
|
||||
for key, value := range vars {
|
||||
cmd.Env = append(cmd.Env, key+"="+value)
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
## Dynamic content
|
||||
#
|
||||
#CGIPaths = [
|
||||
# "^/var/gemini/cgi-bin/",
|
||||
# "^/var/gemini/users/trusted-user/cgi-bin/",
|
||||
# "/var/gemini/cgi-bin",
|
||||
# "/var/gemini/users/*/cgi-bin/", # Unsafe!
|
||||
#]
|
||||
#
|
||||
#[SCGIPaths]
|
||||
|
|
12
handler.go
12
handler.go
|
@ -91,9 +91,17 @@ func handleGeminiRequest(conn net.Conn, config Config, accessLogEntries chan Log
|
|||
}
|
||||
|
||||
// Check whether this URL is in a configured CGI path
|
||||
var cgiPaths []string
|
||||
for _, cgiPath := range config.CGIPaths {
|
||||
inCGIPath, err := regexp.Match(cgiPath, []byte(path))
|
||||
if err == nil && inCGIPath {
|
||||
expandedPaths, err := filepath.Glob(cgiPath)
|
||||
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)
|
||||
if log.Status != 0 {
|
||||
return
|
||||
|
|
Loading…
Add table
Reference in a new issue