diff --git a/net/cgi-io/Makefile b/net/cgi-io/Makefile index 92cca4fa0..5107cd61c 100644 --- a/net/cgi-io/Makefile +++ b/net/cgi-io/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=cgi-io -PKG_RELEASE:=14 +PKG_RELEASE:=15 PKG_LICENSE:=GPL-2.0-or-later @@ -40,6 +40,7 @@ define Package/cgi-io/install $(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-upload $(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-download $(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-backup + $(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-exec endef $(eval $(call BuildPackage,cgi-io)) diff --git a/net/cgi-io/src/main.c b/net/cgi-io/src/main.c index 3530284c6..778dc4c63 100644 --- a/net/cgi-io/src/main.c +++ b/net/cgi-io/src/main.c @@ -804,6 +804,237 @@ main_backup(int argc, char **argv) } } + +static const char * +lookup_executable(const char *cmd) +{ + size_t plen = 0, clen = strlen(cmd) + 1; + static char path[PATH_MAX]; + char *search, *p; + struct stat s; + + if (!stat(cmd, &s) && S_ISREG(s.st_mode)) + return cmd; + + search = getenv("PATH"); + + if (!search) + search = "/bin:/usr/bin:/sbin:/usr/sbin"; + + p = search; + + do { + if (*p != ':' && *p != '\0') + continue; + + plen = p - search; + + if ((plen + clen) >= sizeof(path)) + continue; + + strncpy(path, search, plen); + sprintf(path + plen, "/%s", cmd); + + if (!stat(path, &s) && S_ISREG(s.st_mode)) + return path; + + search = p + 1; + } while (*p++); + + return NULL; +} + +static char ** +parse_command(const char *cmdline) +{ + const char *p = cmdline, *s; + char **argv = NULL, *out; + size_t arglen = 0; + int argnum = 0; + bool esc; + + while (isspace(*cmdline)) + cmdline++; + + for (p = cmdline, s = p, esc = false; p; p++) { + if (esc) { + esc = false; + } + else if (*p == '\\' && p[1] != 0) { + esc = true; + } + else if (isspace(*p) || *p == 0) { + if (p > s) { + argnum += 1; + arglen += sizeof(char *) + (p - s) + 1; + } + + s = p + 1; + } + + if (*p == 0) + break; + } + + if (arglen == 0) + return NULL; + + argv = calloc(1, arglen + sizeof(char *)); + + if (!argv) + return NULL; + + out = (char *)argv + sizeof(char *) * (argnum + 1); + argv[0] = out; + + for (p = cmdline, s = p, esc = false, argnum = 0; p; p++) { + if (esc) { + esc = false; + *out++ = *p; + } + else if (*p == '\\' && p[1] != 0) { + esc = true; + } + else if (isspace(*p) || *p == 0) { + if (p > s) { + *out++ = ' '; + argv[++argnum] = out; + } + + s = p + 1; + } + else { + *out++ = *p; + } + + if (*p == 0) + break; + } + + argv[argnum] = NULL; + out[-1] = 0; + + return argv; +} + +static int +main_exec(int argc, char **argv) +{ + char *fields[] = { "sessionid", NULL, "command", NULL, "filename", NULL, "mimetype", NULL }; + int i, devnull, status, fds[2]; + bool allowed = false; + ssize_t len = 0; + const char *exe; + char *p, **args; + pid_t pid; + + postdecode(fields, 4); + + if (!fields[1] || !session_access(fields[1], "cgi-io", "exec", "read")) + return failure(403, 0, "Exec permission denied"); + + for (p = fields[5]; p && *p; p++) + if (!isalnum(*p) && !strchr(" ()<>@,;:[]?.=%-", *p)) + return failure(400, 0, "Invalid characters in filename"); + + for (p = fields[7]; p && *p; p++) + if (!isalnum(*p) && !strchr(" .;=/-", *p)) + return failure(400, 0, "Invalid characters in mimetype"); + + args = fields[3] ? parse_command(fields[3]) : NULL; + + if (!args) + return failure(400, 0, "Invalid command parameter"); + + /* First check if we find an ACL match for the whole cmdline ... */ + allowed = session_access(fields[1], "file", args[0], "exec"); + + /* Now split the command vector... */ + for (i = 1; args[i]; i++) + args[i][-1] = 0; + + /* Find executable... */ + exe = lookup_executable(args[0]); + + if (!exe) { + free(args); + return failure(404, 0, "Executable not found"); + } + + /* If there was no ACL match, check for a match on the executable */ + if (!allowed && !session_access(fields[1], "file", exe, "exec")) { + free(args); + return failure(403, 0, "Access to command denied by ACL"); + } + + if (pipe(fds)) { + free(args); + return failure(500, errno, "Failed to spawn pipe"); + } + + switch ((pid = fork())) + { + case -1: + free(args); + close(fds[0]); + close(fds[1]); + return failure(500, errno, "Failed to fork process"); + + case 0: + devnull = open("/dev/null", O_RDWR); + + if (devnull > -1) { + dup2(devnull, 0); + dup2(devnull, 2); + close(devnull); + } + else { + close(0); + close(2); + } + + dup2(fds[1], 1); + close(fds[0]); + close(fds[1]); + + if (chdir("/") < 0) { + free(args); + return failure(500, errno, "Failed chdir('/')"); + } + + if (execv(exe, args) < 0) { + free(args); + return failure(500, errno, "Failed execv(...)"); + } + + return -1; + + default: + printf("Status: 200 OK\r\n"); + printf("Content-Type: %s\r\n", + fields[7] ? fields[7] : "application/octet-stream"); + + if (fields[5]) + printf("Content-Disposition: attachment; filename=\"%s\"\r\n", + fields[5]); + + printf("\r\n"); + fflush(stdout); + + do { + len = splice(fds[0], NULL, 1, NULL, READ_BLOCK, SPLICE_F_MORE); + } while (len > 0); + + waitpid(pid, &status, 0); + + close(fds[0]); + close(fds[1]); + free(args); + + return 0; + } +} + int main(int argc, char **argv) { if (strstr(argv[0], "cgi-upload")) @@ -812,6 +1043,8 @@ int main(int argc, char **argv) return main_download(argc, argv); else if (strstr(argv[0], "cgi-backup")) return main_backup(argc, argv); + else if (strstr(argv[0], "cgi-exec")) + return main_exec(argc, argv); return -1; }