├── .gitignore ├── tests ├── fuzz-multipart-parser │ ├── corpus │ │ └── .keep │ ├── inputs │ │ ├── input1.txt │ │ └── input2.txt │ ├── dict │ │ └── parser.dict │ ├── CMakeLists.txt │ └── test-fuzz-multipart-parser.c ├── cram │ ├── test-cases │ │ ├── cgi-exec-01.txt │ │ ├── cgi-exec-02.txt │ │ └── cgi-exec-03.txt │ ├── CMakeLists.txt │ ├── test-san_cgi-exec.t │ └── test_cgi-exec.t ├── fuzz │ ├── corpus │ │ ├── 58668e7669fd564d99db5d581fcdb6a5618440b5 │ │ ├── 5ba93c9db0cff93f52b521d7420e43f6eda2784f │ │ ├── adc83b19e793491b1c6ea0fd8b46cd9f32e592fc │ │ ├── crash-9adc1b00fe9189d66d3bfd8b7759b003cf3f5427 │ │ └── crash-c1e3b9cd71f83cc0de5ab4c0e3db39316cd5c6c0 │ ├── CMakeLists.txt │ └── test-fuzz.c └── CMakeLists.txt ├── .gitlab-ci.yml ├── util.h ├── multipart_parser.h ├── CMakeLists.txt ├── util.c ├── multipart_parser.c └── main.c /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /tests/fuzz-multipart-parser/corpus/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/cram/test-cases/cgi-exec-01.txt: -------------------------------------------------------------------------------- 1 | sessionid=0& 2 | -------------------------------------------------------------------------------- /tests/fuzz/corpus/58668e7669fd564d99db5d581fcdb6a5618440b5: -------------------------------------------------------------------------------- 1 | J -------------------------------------------------------------------------------- /tests/fuzz/corpus/5ba93c9db0cff93f52b521d7420e43f6eda2784f: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fuzz/corpus/adc83b19e793491b1c6ea0fd8b46cd9f32e592fc: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/cram/test-cases/cgi-exec-02.txt: -------------------------------------------------------------------------------- 1 | sessionid=0&command=basename /tmp/foo& 2 | -------------------------------------------------------------------------------- /tests/cram/test-cases/cgi-exec-03.txt: -------------------------------------------------------------------------------- 1 | sessionid=0&command=basename /king/banik/1922&filename=output.txt&mimetype=0& 2 | -------------------------------------------------------------------------------- /tests/fuzz/corpus/crash-9adc1b00fe9189d66d3bfd8b7759b003cf3f5427: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwrt/cgi-io/master/tests/fuzz/corpus/crash-9adc1b00fe9189d66d3bfd8b7759b003cf3f5427 -------------------------------------------------------------------------------- /tests/fuzz/corpus/crash-c1e3b9cd71f83cc0de5ab4c0e3db39316cd5c6c0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwrt/cgi-io/master/tests/fuzz/corpus/crash-c1e3b9cd71f83cc0de5ab4c0e3db39316cd5c6c0 -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ADD_SUBDIRECTORY(cram) 2 | 3 | IF(CMAKE_C_COMPILER_ID STREQUAL "Clang") 4 | ADD_SUBDIRECTORY(fuzz) 5 | ADD_SUBDIRECTORY(fuzz-multipart-parser) 6 | ENDIF() 7 | -------------------------------------------------------------------------------- /tests/fuzz-multipart-parser/inputs/input1.txt: -------------------------------------------------------------------------------- 1 | --AaB03x 2 | Content-Disposition: form-data; name="files"; filename="fi;le1.txt" 3 | Content-Type: text/plain 4 | 5 | contents 6 | --AaB03x-- -------------------------------------------------------------------------------- /tests/fuzz-multipart-parser/dict/parser.dict: -------------------------------------------------------------------------------- 1 | "Content-Disposition: form-data; name=" 2 | "\x0D\x0A" 3 | "\x0D" 4 | "x0A" 5 | "=" 6 | ";" 7 | "--" 8 | "Content-Type:" 9 | "filename=" 10 | "--12--34--56" 11 | -------------------------------------------------------------------------------- /tests/fuzz-multipart-parser/inputs/input2.txt: -------------------------------------------------------------------------------- 1 | --AaB03x 2 | Content-Disposition: form-data; name="submit-name" 3 | 4 | Larry 5 | --AaB03x 6 | Content-Disposition: form-data; name="files"; filename="file1.txt" 7 | Content-Type: text/plain 8 | 9 | 10 | --AaB03x-- 11 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | CI_ENABLE_UNIT_TESTING: 1 3 | CI_TARGET_BUILD_DEPENDS: ubus 4 | 5 | include: 6 | - remote: https://gitlab.com/ynezz/openwrt-ci/raw/master/openwrt-ci/gitlab/main.yml 7 | - remote: https://gitlab.com/ynezz/openwrt-ci/raw/master/openwrt-ci/gitlab/pipeline.yml 8 | -------------------------------------------------------------------------------- /util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define POST_LIMIT 131072 6 | 7 | char** parse_command(const char *cmdline); 8 | char* postdecode(char **fields, int n_fields); 9 | char* postdecode_fields(char *postbuf, ssize_t len, char **fields, int n_fields); 10 | char* canonicalize_path(const char *path, size_t len); 11 | bool urldecode(char *buf); 12 | char* datadup(const void *in, size_t len); 13 | -------------------------------------------------------------------------------- /tests/fuzz/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | FILE(GLOB test_cases "test-*.c") 2 | 3 | MACRO(ADD_FUZZER_TEST name) 4 | ADD_EXECUTABLE(${name} ${name}.c) 5 | TARGET_COMPILE_OPTIONS(${name} PRIVATE -g -O1 -fno-omit-frame-pointer -fsanitize=fuzzer,address,leak,undefined) 6 | TARGET_INCLUDE_DIRECTORIES(${name} PRIVATE ${PROJECT_SOURCE_DIR}) 7 | TARGET_LINK_OPTIONS(${name} PRIVATE -stdlib=libc++ -fsanitize=fuzzer,address,leak,undefined) 8 | TARGET_LINK_LIBRARIES(${name} cgi-lib) 9 | ADD_TEST( 10 | NAME ${name} 11 | COMMAND ${name} -max_len=256 -timeout=10 -max_total_time=300 ${CMAKE_CURRENT_SOURCE_DIR}/corpus 12 | ) 13 | ENDMACRO(ADD_FUZZER_TEST) 14 | 15 | FOREACH(test_case ${test_cases}) 16 | GET_FILENAME_COMPONENT(test_case ${test_case} NAME_WE) 17 | ADD_FUZZER_TEST(${test_case}) 18 | ENDFOREACH(test_case) 19 | -------------------------------------------------------------------------------- /tests/fuzz-multipart-parser/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | FILE(GLOB test_cases "test-*.c") 2 | 3 | MACRO(ADD_FUZZER_TEST name) 4 | ADD_EXECUTABLE(${name} ${name}.c) 5 | TARGET_COMPILE_OPTIONS(${name} PRIVATE -g -O1 -fno-omit-frame-pointer -fsanitize=fuzzer,address,leak,undefined) 6 | TARGET_INCLUDE_DIRECTORIES(${name} PRIVATE ${PROJECT_SOURCE_DIR}) 7 | TARGET_LINK_OPTIONS(${name} PRIVATE -stdlib=libc++ -fsanitize=fuzzer,address,leak,undefined) 8 | TARGET_LINK_LIBRARIES(${name} cgi-lib) 9 | ADD_TEST( 10 | NAME ${name} 11 | COMMAND ${name} -max_len=256 -timeout=10 -max_total_time=300 -dict=${CMAKE_CURRENT_SOURCE_DIR}/dict/parser.dict ${CMAKE_CURRENT_SOURCE_DIR}/corpus 12 | ) 13 | ENDMACRO(ADD_FUZZER_TEST) 14 | 15 | FOREACH(test_case ${test_cases}) 16 | GET_FILENAME_COMPONENT(test_case ${test_case} NAME_WE) 17 | ADD_FUZZER_TEST(${test_case}) 18 | ENDFOREACH(test_case) 19 | -------------------------------------------------------------------------------- /tests/cram/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | FIND_PACKAGE(PythonInterp 3 REQUIRED) 2 | FILE(GLOB test_cases "test_*.t") 3 | 4 | IF(CMAKE_C_COMPILER_ID STREQUAL "Clang") 5 | FILE(GLOB test_cases_san "test-san_*.t") 6 | ENDIF() 7 | 8 | SET(PYTHON_VENV_DIR "${CMAKE_CURRENT_BINARY_DIR}/.venv") 9 | SET(PYTHON_VENV_PIP "${PYTHON_VENV_DIR}/bin/pip") 10 | SET(PYTHON_VENV_CRAM "${PYTHON_VENV_DIR}/bin/cram") 11 | 12 | ADD_CUSTOM_COMMAND( 13 | OUTPUT ${PYTHON_VENV_CRAM} 14 | COMMAND ${PYTHON_EXECUTABLE} -m venv ${PYTHON_VENV_DIR} 15 | COMMAND ${PYTHON_VENV_PIP} install cram 16 | ) 17 | ADD_CUSTOM_TARGET(prepare-cram-venv ALL DEPENDS ${PYTHON_VENV_CRAM}) 18 | 19 | ADD_TEST( 20 | NAME cram 21 | COMMAND ${PYTHON_VENV_CRAM} ${test_cases} ${test_cases_san} 22 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 23 | ) 24 | 25 | SET_PROPERTY(TEST cram APPEND PROPERTY ENVIRONMENT "BUILD_BIN_DIR=$") 26 | -------------------------------------------------------------------------------- /tests/fuzz/test-fuzz.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include "util.h" 15 | 16 | static void fuzz_parse_command(const char *buf) 17 | { 18 | char **p = parse_command(buf); 19 | if (p) 20 | free(p); 21 | } 22 | 23 | int LLVMFuzzerTestOneInput(const uint8_t *input, size_t size) 24 | { 25 | char *p = NULL; 26 | char *fields[] = { "sessionid", NULL, "path", NULL, "filename", NULL, "mimetype", NULL }; 27 | char *buf = calloc(1, size+1); 28 | memcpy(buf, input, size); 29 | 30 | urldecode(buf); 31 | fuzz_parse_command(buf); 32 | p = canonicalize_path(buf, size+1); 33 | if (p) 34 | free(p); 35 | 36 | p = postdecode_fields(buf, size+1, fields, 4); 37 | if (!p) 38 | return 0; 39 | 40 | free(buf); 41 | 42 | return 0; 43 | } 44 | -------------------------------------------------------------------------------- /tests/cram/test-san_cgi-exec.t: -------------------------------------------------------------------------------- 1 | check that cgi-exec is producing expected results: 2 | 3 | $ [ -n "$BUILD_BIN_DIR" ] && export PATH="$BUILD_BIN_DIR:$PATH" 4 | $ ln -sf $BUILD_BIN_DIR/cgi-io-san $BUILD_BIN_DIR/cgi-exec 5 | 6 | $ for file in $(LC_ALL=C find "$TESTDIR/test-cases" -type f | sort); do 7 | > export CONTENT_TYPE="application/x-www-form-urlencoded"; \ 8 | > export CONTENT_LENGTH="$(wc -c < $file)"; \ 9 | > printf "\n[-] testing: $(basename $file)\n"; \ 10 | > cgi-exec < $file; \ 11 | > done 12 | 13 | [-] testing: cgi-exec-01.txt 14 | Status: 400 Invalid command parameter\r (esc) 15 | Content-Type: text/plain\r (esc) 16 | \r (esc) 17 | Invalid command parameter 18 | 19 | [-] testing: cgi-exec-02.txt 20 | Status: 200 OK\r (esc) 21 | Content-Type: application/octet-stream\r (esc) 22 | \r (esc) 23 | foo 24 | 25 | [-] testing: cgi-exec-03.txt 26 | Status: 200 OK\r (esc) 27 | Content-Type: 0\r (esc) 28 | Content-Disposition: attachment; filename="output.txt"\r (esc) 29 | \r (esc) 30 | 1922 31 | -------------------------------------------------------------------------------- /tests/fuzz-multipart-parser/test-fuzz-multipart-parser.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include "multipart_parser.h" 15 | 16 | int LLVMFuzzerTestOneInput(const uint8_t *input, size_t size) 17 | { 18 | char *buf = NULL; 19 | multipart_parser *p; 20 | static multipart_parser_settings s = { 21 | .on_part_data = NULL, 22 | .on_headers_complete = NULL, 23 | .on_part_data_end = NULL, 24 | .on_header_field = NULL, 25 | .on_header_value = NULL, 26 | }; 27 | buf = calloc(1, size + 1); 28 | if (!buf) 29 | return 0; 30 | 31 | memcpy(buf, input, size); 32 | p = multipart_parser_init(buf, &s); 33 | if (!p) { 34 | free(buf); 35 | return 0; 36 | } 37 | 38 | multipart_parser_execute(p, buf, size + 1); 39 | multipart_parser_free(p); 40 | free(buf); 41 | 42 | return 0; 43 | } 44 | -------------------------------------------------------------------------------- /tests/cram/test_cgi-exec.t: -------------------------------------------------------------------------------- 1 | check that cgi-exec is producing expected results: 2 | 3 | $ [ -n "$BUILD_BIN_DIR" ] && export PATH="$BUILD_BIN_DIR:$PATH" 4 | $ ln -sf $BUILD_BIN_DIR/cgi-io $BUILD_BIN_DIR/cgi-exec 5 | 6 | $ for file in $(LC_ALL=C find "$TESTDIR/test-cases" -type f | sort); do 7 | > export CONTENT_TYPE="application/x-www-form-urlencoded"; \ 8 | > export CONTENT_LENGTH="$(wc -c < $file)"; \ 9 | > printf "\n[-] testing: $(basename $file)\n"; \ 10 | > valgrind --quiet --leak-check=full cgi-exec < $file; \ 11 | > done 12 | 13 | [-] testing: cgi-exec-01.txt 14 | Status: 400 Invalid command parameter\r (esc) 15 | Content-Type: text/plain\r (esc) 16 | \r (esc) 17 | Invalid command parameter 18 | 19 | [-] testing: cgi-exec-02.txt 20 | Status: 200 OK\r (esc) 21 | Content-Type: application/octet-stream\r (esc) 22 | \r (esc) 23 | foo 24 | 25 | [-] testing: cgi-exec-03.txt 26 | Status: 200 OK\r (esc) 27 | Content-Type: 0\r (esc) 28 | Content-Disposition: attachment; filename="output.txt"\r (esc) 29 | \r (esc) 30 | 1922 31 | -------------------------------------------------------------------------------- /multipart_parser.h: -------------------------------------------------------------------------------- 1 | /* Based on node-formidable by Felix Geisendörfer 2 | * Igor Afonov - afonov@gmail.com - 2012 3 | * MIT License - http://www.opensource.org/licenses/mit-license.php 4 | */ 5 | #ifndef _multipart_parser_h 6 | #define _multipart_parser_h 7 | 8 | #ifdef __cplusplus 9 | extern "C" 10 | { 11 | #endif 12 | 13 | #include 14 | #include 15 | 16 | typedef struct multipart_parser multipart_parser; 17 | typedef struct multipart_parser_settings multipart_parser_settings; 18 | typedef struct multipart_parser_state multipart_parser_state; 19 | 20 | typedef int (*multipart_data_cb) (multipart_parser*, const char *at, size_t length); 21 | typedef int (*multipart_notify_cb) (multipart_parser*); 22 | 23 | struct multipart_parser_settings { 24 | multipart_data_cb on_header_field; 25 | multipart_data_cb on_header_value; 26 | multipart_data_cb on_part_data; 27 | 28 | multipart_notify_cb on_part_data_begin; 29 | multipart_notify_cb on_headers_complete; 30 | multipart_notify_cb on_part_data_end; 31 | multipart_notify_cb on_body_end; 32 | }; 33 | 34 | multipart_parser* multipart_parser_init 35 | (const char *boundary, const multipart_parser_settings* settings); 36 | 37 | void multipart_parser_free(multipart_parser* p); 38 | 39 | size_t multipart_parser_execute(multipart_parser* p, const char *buf, size_t len); 40 | 41 | void multipart_parser_set_data(multipart_parser* p, void* data); 42 | void * multipart_parser_get_data(multipart_parser* p); 43 | 44 | #ifdef __cplusplus 45 | } /* extern "C" */ 46 | #endif 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | PROJECT(cgi-io C) 4 | 5 | INCLUDE(CheckFunctionExists) 6 | 7 | FIND_PATH(ubus_include_dir libubus.h) 8 | FIND_LIBRARY(ubox NAMES ubox) 9 | FIND_LIBRARY(ubus NAMES ubus) 10 | INCLUDE_DIRECTORIES(${ubus_include_dir}) 11 | 12 | ADD_DEFINITIONS(-Os -Wall -Werror -Wextra --std=gnu99 -g3) 13 | ADD_DEFINITIONS(-Wno-unused-parameter -Wmissing-declarations) 14 | 15 | SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") 16 | 17 | IF(APPLE) 18 | INCLUDE_DIRECTORIES(/opt/local/include) 19 | LINK_DIRECTORIES(/opt/local/lib) 20 | ENDIF() 21 | 22 | SET(LIB_SOURCES multipart_parser.c util.c) 23 | ADD_LIBRARY(cgi-lib STATIC ${LIB_SOURCES}) 24 | 25 | ADD_EXECUTABLE(cgi-io main.c) 26 | TARGET_LINK_LIBRARIES(cgi-io cgi-lib ${ubox} ${ubus}) 27 | 28 | IF(UNIT_TESTING) 29 | ADD_DEFINITIONS(-DUNIT_TESTING) 30 | ENABLE_TESTING() 31 | ADD_SUBDIRECTORY(tests) 32 | 33 | IF(CMAKE_C_COMPILER_ID STREQUAL "Clang") 34 | ADD_LIBRARY(cgi-lib-san SHARED ${LIB_SOURCES}) 35 | TARGET_COMPILE_OPTIONS(cgi-lib-san PRIVATE -g -fno-omit-frame-pointer -fsanitize=undefined,address,leak -fno-sanitize-recover=all) 36 | TARGET_LINK_OPTIONS(cgi-lib-san PRIVATE -fsanitize=undefined,address,leak) 37 | TARGET_LINK_LIBRARIES(cgi-lib-san ${ubox} ${ubus}) 38 | 39 | ADD_EXECUTABLE(cgi-io-san main.c) 40 | TARGET_COMPILE_OPTIONS(cgi-io-san PRIVATE -g -fno-omit-frame-pointer -fsanitize=undefined,address,leak -fno-sanitize-recover=all) 41 | TARGET_LINK_OPTIONS(cgi-io-san PRIVATE -fsanitize=undefined,address,leak) 42 | TARGET_LINK_LIBRARIES(cgi-io-san cgi-lib-san ${ubox}) 43 | ENDIF() 44 | ENDIF() 45 | 46 | INSTALL(TARGETS cgi-io RUNTIME DESTINATION sbin) 47 | -------------------------------------------------------------------------------- /util.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "util.h" 11 | 12 | char ** 13 | parse_command(const char *cmdline) 14 | { 15 | const char *p = cmdline, *s; 16 | char **argv = NULL, *out; 17 | size_t arglen = 0; 18 | int argnum = 0; 19 | bool esc; 20 | 21 | while (isspace(*cmdline)) 22 | cmdline++; 23 | 24 | for (p = cmdline, s = p, esc = false; p; p++) { 25 | if (esc) { 26 | esc = false; 27 | } 28 | else if (*p == '\\' && p[1] != 0) { 29 | esc = true; 30 | } 31 | else if (isspace(*p) || *p == 0) { 32 | if (p > s) { 33 | argnum += 1; 34 | arglen += sizeof(char *) + (p - s) + 1; 35 | } 36 | 37 | s = p + 1; 38 | } 39 | 40 | if (*p == 0) 41 | break; 42 | } 43 | 44 | if (arglen == 0) 45 | return NULL; 46 | 47 | argv = calloc(1, arglen + sizeof(char *)); 48 | 49 | if (!argv) 50 | return NULL; 51 | 52 | out = (char *)argv + sizeof(char *) * (argnum + 1); 53 | argv[0] = out; 54 | 55 | for (p = cmdline, s = p, esc = false, argnum = 0; p; p++) { 56 | if (esc) { 57 | esc = false; 58 | *out++ = *p; 59 | } 60 | else if (*p == '\\' && p[1] != 0) { 61 | esc = true; 62 | } 63 | else if (isspace(*p) || *p == 0) { 64 | if (p > s) { 65 | *out++ = ' '; 66 | argv[++argnum] = out; 67 | } 68 | 69 | s = p + 1; 70 | } 71 | else { 72 | *out++ = *p; 73 | } 74 | 75 | if (*p == 0) 76 | break; 77 | } 78 | 79 | argv[argnum] = NULL; 80 | out[-1] = 0; 81 | 82 | return argv; 83 | } 84 | 85 | char * 86 | postdecode_fields(char *postbuf, ssize_t len, char **fields, int n_fields) 87 | { 88 | char *p; 89 | int i, field, found = 0; 90 | 91 | for (p = postbuf, i = 0; i < len; i++) 92 | { 93 | if (postbuf[i] == '=') 94 | { 95 | postbuf[i] = 0; 96 | 97 | for (field = 0; field < (n_fields * 2); field += 2) 98 | { 99 | if (!strcmp(p, fields[field])) 100 | { 101 | fields[field + 1] = postbuf + i + 1; 102 | found++; 103 | } 104 | } 105 | } 106 | else if (postbuf[i] == '&' || postbuf[i] == '\0') 107 | { 108 | postbuf[i] = 0; 109 | 110 | if (found >= n_fields) 111 | break; 112 | 113 | p = postbuf + i + 1; 114 | } 115 | } 116 | 117 | for (field = 0; field < (n_fields * 2); field += 2) 118 | { 119 | if (!urldecode(fields[field + 1])) 120 | { 121 | free(postbuf); 122 | return NULL; 123 | } 124 | } 125 | 126 | return postbuf; 127 | } 128 | 129 | char * 130 | postdecode(char **fields, int n_fields) 131 | { 132 | const char *var; 133 | char *p, *postbuf; 134 | ssize_t len = 0, rlen = 0, content_length = 0; 135 | 136 | var = getenv("CONTENT_TYPE"); 137 | 138 | if (!var || strncmp(var, "application/x-www-form-urlencoded", 33)) 139 | return NULL; 140 | 141 | var = getenv("CONTENT_LENGTH"); 142 | 143 | if (!var) 144 | return NULL; 145 | 146 | content_length = strtol(var, &p, 10); 147 | 148 | if (p == var || content_length <= 0 || content_length >= POST_LIMIT) 149 | return NULL; 150 | 151 | postbuf = calloc(1, content_length + 1); 152 | 153 | if (postbuf == NULL) 154 | return NULL; 155 | 156 | for (len = 0; len < content_length; ) 157 | { 158 | rlen = read(0, postbuf + len, content_length - len); 159 | 160 | if (rlen <= 0) 161 | break; 162 | 163 | len += rlen; 164 | } 165 | 166 | if (len < content_length) 167 | { 168 | free(postbuf); 169 | return NULL; 170 | } 171 | 172 | return postdecode_fields(postbuf, len, fields, n_fields); 173 | } 174 | 175 | char * 176 | datadup(const void *in, size_t len) 177 | { 178 | char *out = malloc(len + 1); 179 | 180 | if (!out) 181 | return NULL; 182 | 183 | memcpy(out, in, len); 184 | 185 | *(out + len) = 0; 186 | 187 | return out; 188 | } 189 | 190 | char * 191 | canonicalize_path(const char *path, size_t len) 192 | { 193 | char *canonpath, *cp; 194 | const char *p, *e; 195 | 196 | if (path == NULL || *path == '\0') 197 | return NULL; 198 | 199 | canonpath = datadup(path, len); 200 | 201 | if (canonpath == NULL) 202 | return NULL; 203 | 204 | /* normalize */ 205 | for (cp = canonpath, p = path, e = path + len; p < e; ) { 206 | if (*p != '/') 207 | goto next; 208 | 209 | /* skip repeating / */ 210 | if ((p + 1 < e) && (p[1] == '/')) { 211 | p++; 212 | continue; 213 | } 214 | 215 | /* /./ or /../ */ 216 | if ((p + 1 < e) && (p[1] == '.')) { 217 | /* skip /./ */ 218 | if ((p + 2 >= e) || (p[2] == '/')) { 219 | p += 2; 220 | continue; 221 | } 222 | 223 | /* collapse /x/../ */ 224 | if ((p + 2 < e) && (p[2] == '.') && ((p + 3 >= e) || (p[3] == '/'))) { 225 | while ((cp > canonpath) && (*--cp != '/')) 226 | ; 227 | 228 | p += 3; 229 | continue; 230 | } 231 | } 232 | 233 | next: 234 | *cp++ = *p++; 235 | } 236 | 237 | /* remove trailing slash if not root / */ 238 | if ((cp > canonpath + 1) && (cp[-1] == '/')) 239 | cp--; 240 | else if (cp == canonpath) 241 | *cp++ = '/'; 242 | 243 | *cp = '\0'; 244 | 245 | return canonpath; 246 | } 247 | 248 | bool 249 | urldecode(char *buf) 250 | { 251 | char *c, *p; 252 | 253 | if (!buf || !*buf) 254 | return true; 255 | 256 | #define hex(x) \ 257 | (((x) <= '9') ? ((x) - '0') : \ 258 | (((x) <= 'F') ? ((x) - 'A' + 10) : \ 259 | ((x) - 'a' + 10))) 260 | 261 | for (c = p = buf; *p; c++) 262 | { 263 | if (*p == '%') 264 | { 265 | if (!isxdigit(*(p + 1)) || !isxdigit(*(p + 2))) 266 | return false; 267 | 268 | *c = (char)(16 * hex(*(p + 1)) + hex(*(p + 2))); 269 | 270 | p += 3; 271 | } 272 | else if (*p == '+') 273 | { 274 | *c = ' '; 275 | p++; 276 | } 277 | else 278 | { 279 | *c = *p++; 280 | } 281 | } 282 | 283 | *c = 0; 284 | 285 | return true; 286 | } 287 | -------------------------------------------------------------------------------- /multipart_parser.c: -------------------------------------------------------------------------------- 1 | /* Based on node-formidable by Felix Geisendörfer 2 | * Igor Afonov - afonov@gmail.com - 2012 3 | * MIT License - http://www.opensource.org/licenses/mit-license.php 4 | */ 5 | 6 | #include "multipart_parser.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | static void multipart_log(const char * format, ...) 13 | { 14 | #ifdef DEBUG_MULTIPART 15 | va_list args; 16 | va_start(args, format); 17 | 18 | fprintf(stderr, "[HTTP_MULTIPART_PARSER] %s:%d: ", __FILE__, __LINE__); 19 | vfprintf(stderr, format, args); 20 | fprintf(stderr, "\n"); 21 | #endif 22 | } 23 | 24 | #define NOTIFY_CB(FOR) \ 25 | do { \ 26 | if (p->settings->on_##FOR) { \ 27 | if (p->settings->on_##FOR(p) != 0) { \ 28 | return i; \ 29 | } \ 30 | } \ 31 | } while (0) 32 | 33 | #define EMIT_DATA_CB(FOR, ptr, len) \ 34 | do { \ 35 | if (p->settings->on_##FOR) { \ 36 | if (p->settings->on_##FOR(p, ptr, len) != 0) { \ 37 | return i; \ 38 | } \ 39 | } \ 40 | } while (0) 41 | 42 | 43 | #define LF 10 44 | #define CR 13 45 | 46 | struct multipart_parser { 47 | void * data; 48 | 49 | size_t index; 50 | size_t boundary_length; 51 | 52 | unsigned char state; 53 | 54 | const multipart_parser_settings* settings; 55 | 56 | char* lookbehind; 57 | char multipart_boundary[1]; 58 | }; 59 | 60 | enum state { 61 | s_uninitialized = 1, 62 | s_start, 63 | s_start_boundary, 64 | s_header_field_start, 65 | s_header_field, 66 | s_headers_almost_done, 67 | s_header_value_start, 68 | s_header_value, 69 | s_header_value_almost_done, 70 | s_part_data_start, 71 | s_part_data, 72 | s_part_data_almost_boundary, 73 | s_part_data_boundary, 74 | s_part_data_almost_end, 75 | s_part_data_end, 76 | s_part_data_final_hyphen, 77 | s_end 78 | }; 79 | 80 | multipart_parser* multipart_parser_init 81 | (const char *boundary, const multipart_parser_settings* settings) { 82 | 83 | multipart_parser* p = malloc(sizeof(multipart_parser) + 84 | strlen(boundary) + 85 | strlen(boundary) + 9); 86 | 87 | if (!p) 88 | return NULL; 89 | 90 | strcpy(p->multipart_boundary, boundary); 91 | p->boundary_length = strlen(boundary); 92 | 93 | p->lookbehind = (p->multipart_boundary + p->boundary_length + 1); 94 | 95 | p->index = 0; 96 | p->state = s_start; 97 | p->settings = settings; 98 | 99 | return p; 100 | } 101 | 102 | void multipart_parser_free(multipart_parser* p) { 103 | free(p); 104 | } 105 | 106 | void multipart_parser_set_data(multipart_parser *p, void *data) { 107 | p->data = data; 108 | } 109 | 110 | void *multipart_parser_get_data(multipart_parser *p) { 111 | return p->data; 112 | } 113 | 114 | size_t multipart_parser_execute(multipart_parser* p, const char *buf, size_t len) { 115 | size_t i = 0; 116 | size_t mark = 0; 117 | char c, cl; 118 | int is_last = 0; 119 | 120 | while(i < len) { 121 | c = buf[i]; 122 | is_last = (i == (len - 1)); 123 | switch (p->state) { 124 | case s_start: 125 | multipart_log("s_start"); 126 | p->index = 0; 127 | p->state = s_start_boundary; 128 | 129 | /* fallthrough */ 130 | case s_start_boundary: 131 | multipart_log("s_start_boundary"); 132 | if (p->index == p->boundary_length) { 133 | if (c != CR) { 134 | return i; 135 | } 136 | p->index++; 137 | break; 138 | } else if (p->index == (p->boundary_length + 1)) { 139 | if (c != LF) { 140 | return i; 141 | } 142 | p->index = 0; 143 | NOTIFY_CB(part_data_begin); 144 | p->state = s_header_field_start; 145 | break; 146 | } 147 | if (c != p->multipart_boundary[p->index]) { 148 | return i; 149 | } 150 | p->index++; 151 | break; 152 | 153 | case s_header_field_start: 154 | multipart_log("s_header_field_start"); 155 | mark = i; 156 | p->state = s_header_field; 157 | 158 | /* fallthrough */ 159 | case s_header_field: 160 | multipart_log("s_header_field"); 161 | if (c == CR) { 162 | p->state = s_headers_almost_done; 163 | break; 164 | } 165 | 166 | if (c == '-') { 167 | break; 168 | } 169 | 170 | if (c == ':') { 171 | EMIT_DATA_CB(header_field, buf + mark, i - mark); 172 | p->state = s_header_value_start; 173 | break; 174 | } 175 | 176 | cl = tolower(c); 177 | if (cl < 'a' || cl > 'z') { 178 | multipart_log("invalid character in header name"); 179 | return i; 180 | } 181 | if (is_last) 182 | EMIT_DATA_CB(header_field, buf + mark, (i - mark) + 1); 183 | break; 184 | 185 | case s_headers_almost_done: 186 | multipart_log("s_headers_almost_done"); 187 | if (c != LF) { 188 | return i; 189 | } 190 | 191 | p->state = s_part_data_start; 192 | break; 193 | 194 | case s_header_value_start: 195 | multipart_log("s_header_value_start"); 196 | if (c == ' ') { 197 | break; 198 | } 199 | 200 | mark = i; 201 | p->state = s_header_value; 202 | 203 | /* fallthrough */ 204 | case s_header_value: 205 | multipart_log("s_header_value"); 206 | if (c == CR) { 207 | EMIT_DATA_CB(header_value, buf + mark, i - mark); 208 | p->state = s_header_value_almost_done; 209 | } 210 | if (is_last) 211 | EMIT_DATA_CB(header_value, buf + mark, (i - mark) + 1); 212 | break; 213 | 214 | case s_header_value_almost_done: 215 | multipart_log("s_header_value_almost_done"); 216 | if (c != LF) { 217 | return i; 218 | } 219 | p->state = s_header_field_start; 220 | break; 221 | 222 | case s_part_data_start: 223 | multipart_log("s_part_data_start"); 224 | NOTIFY_CB(headers_complete); 225 | mark = i; 226 | p->state = s_part_data; 227 | 228 | /* fallthrough */ 229 | case s_part_data: 230 | multipart_log("s_part_data"); 231 | if (c == CR) { 232 | EMIT_DATA_CB(part_data, buf + mark, i - mark); 233 | mark = i; 234 | p->state = s_part_data_almost_boundary; 235 | p->lookbehind[0] = CR; 236 | break; 237 | } 238 | if (is_last) 239 | EMIT_DATA_CB(part_data, buf + mark, (i - mark) + 1); 240 | break; 241 | 242 | case s_part_data_almost_boundary: 243 | multipart_log("s_part_data_almost_boundary"); 244 | if (c == LF) { 245 | p->state = s_part_data_boundary; 246 | p->lookbehind[1] = LF; 247 | p->index = 0; 248 | break; 249 | } 250 | EMIT_DATA_CB(part_data, p->lookbehind, 1); 251 | p->state = s_part_data; 252 | mark = i --; 253 | break; 254 | 255 | case s_part_data_boundary: 256 | multipart_log("s_part_data_boundary"); 257 | if (p->multipart_boundary[p->index] != c) { 258 | EMIT_DATA_CB(part_data, p->lookbehind, 2 + p->index); 259 | p->state = s_part_data; 260 | mark = i --; 261 | break; 262 | } 263 | p->lookbehind[2 + p->index] = c; 264 | if ((++ p->index) == p->boundary_length) { 265 | NOTIFY_CB(part_data_end); 266 | p->state = s_part_data_almost_end; 267 | } 268 | break; 269 | 270 | case s_part_data_almost_end: 271 | multipart_log("s_part_data_almost_end"); 272 | if (c == '-') { 273 | p->state = s_part_data_final_hyphen; 274 | break; 275 | } 276 | if (c == CR) { 277 | p->state = s_part_data_end; 278 | break; 279 | } 280 | return i; 281 | 282 | case s_part_data_final_hyphen: 283 | multipart_log("s_part_data_final_hyphen"); 284 | if (c == '-') { 285 | NOTIFY_CB(body_end); 286 | p->state = s_end; 287 | break; 288 | } 289 | return i; 290 | 291 | case s_part_data_end: 292 | multipart_log("s_part_data_end"); 293 | if (c == LF) { 294 | p->state = s_header_field_start; 295 | NOTIFY_CB(part_data_begin); 296 | break; 297 | } 298 | return i; 299 | 300 | case s_end: 301 | multipart_log("s_end: %02X", (int) c); 302 | break; 303 | 304 | default: 305 | multipart_log("Multipart parser unrecoverable error"); 306 | return 0; 307 | } 308 | ++ i; 309 | } 310 | 311 | return len; 312 | } 313 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * cgi-io - LuCI non-RPC helper 3 | * 4 | * Copyright (C) 2013 Jo-Philipp Wich 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | #define _GNU_SOURCE /* splice(), SPLICE_F_MORE */ 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | #include 36 | #include 37 | 38 | #include "util.h" 39 | #include "multipart_parser.h" 40 | 41 | #ifndef O_TMPFILE 42 | #define O_TMPFILE (020000000 | O_DIRECTORY) 43 | #endif 44 | 45 | #define READ_BLOCK 4096 46 | 47 | enum part { 48 | PART_UNKNOWN, 49 | PART_SESSIONID, 50 | PART_FILENAME, 51 | PART_FILEMODE, 52 | PART_FILEDATA 53 | }; 54 | 55 | const char *parts[] = { 56 | "(bug)", 57 | "sessionid", 58 | "filename", 59 | "filemode", 60 | "filedata", 61 | }; 62 | 63 | struct state 64 | { 65 | bool is_content_disposition; 66 | enum part parttype; 67 | char *sessionid; 68 | char *filename; 69 | bool filedata; 70 | int filemode; 71 | int filefd; 72 | int tempfd; 73 | }; 74 | 75 | static struct state st; 76 | 77 | #ifndef UNIT_TESTING 78 | 79 | enum { 80 | SES_ACCESS, 81 | __SES_MAX, 82 | }; 83 | 84 | static const struct blobmsg_policy ses_policy[__SES_MAX] = { 85 | [SES_ACCESS] = { .name = "access", .type = BLOBMSG_TYPE_BOOL }, 86 | }; 87 | 88 | static void 89 | session_access_cb(struct ubus_request *req, int type, struct blob_attr *msg) 90 | { 91 | struct blob_attr *tb[__SES_MAX]; 92 | bool *allow = (bool *)req->priv; 93 | 94 | if (!msg) 95 | return; 96 | 97 | blobmsg_parse(ses_policy, __SES_MAX, tb, blob_data(msg), blob_len(msg)); 98 | 99 | if (tb[SES_ACCESS]) 100 | *allow = blobmsg_get_bool(tb[SES_ACCESS]); 101 | } 102 | #endif 103 | 104 | static bool 105 | session_access(const char *sid, const char *scope, const char *obj, const char *func) 106 | { 107 | #ifdef UNIT_TESTING 108 | return true; 109 | #else 110 | uint32_t id; 111 | bool allow = false; 112 | struct ubus_context *ctx; 113 | static struct blob_buf req; 114 | 115 | ctx = ubus_connect(NULL); 116 | 117 | if (!ctx || !obj || ubus_lookup_id(ctx, "session", &id)) 118 | goto out; 119 | 120 | blob_buf_init(&req, 0); 121 | blobmsg_add_string(&req, "ubus_rpc_session", sid); 122 | blobmsg_add_string(&req, "scope", scope); 123 | blobmsg_add_string(&req, "object", obj); 124 | blobmsg_add_string(&req, "function", func); 125 | 126 | ubus_invoke(ctx, id, "access", req.head, session_access_cb, &allow, 500); 127 | 128 | out: 129 | if (ctx) 130 | ubus_free(ctx); 131 | 132 | return allow; 133 | #endif 134 | } 135 | 136 | static char * 137 | checksum(const char *applet, size_t sumlen, const char *file) 138 | { 139 | pid_t pid; 140 | int r; 141 | int fds[2]; 142 | static char chksum[65]; 143 | 144 | if (pipe(fds)) 145 | return NULL; 146 | 147 | switch ((pid = fork())) 148 | { 149 | case -1: 150 | return NULL; 151 | 152 | case 0: 153 | uloop_done(); 154 | 155 | dup2(fds[1], 1); 156 | 157 | close(0); 158 | close(2); 159 | close(fds[0]); 160 | close(fds[1]); 161 | 162 | if (execl("/bin/busybox", "/bin/busybox", applet, file, NULL)) 163 | return NULL; 164 | 165 | break; 166 | 167 | default: 168 | memset(chksum, 0, sizeof(chksum)); 169 | r = read(fds[0], chksum, sumlen); 170 | 171 | waitpid(pid, NULL, 0); 172 | close(fds[0]); 173 | close(fds[1]); 174 | 175 | if (r < 0) 176 | return NULL; 177 | } 178 | 179 | return chksum; 180 | } 181 | 182 | static int 183 | response(bool success, const char *message) 184 | { 185 | char *chksum; 186 | struct stat s; 187 | 188 | printf("Status: 200 OK\r\n"); 189 | printf("Content-Type: text/plain\r\n\r\n{\n"); 190 | 191 | if (success) 192 | { 193 | if (!stat(st.filename, &s)) 194 | printf("\t\"size\": %u,\n", (unsigned int)s.st_size); 195 | else 196 | printf("\t\"size\": null,\n"); 197 | 198 | chksum = checksum("md5sum", 32, st.filename); 199 | printf("\t\"checksum\": %s%s%s,\n", 200 | chksum ? "\"" : "", 201 | chksum ? chksum : "null", 202 | chksum ? "\"" : ""); 203 | 204 | chksum = checksum("sha256sum", 64, st.filename); 205 | printf("\t\"sha256sum\": %s%s%s\n", 206 | chksum ? "\"" : "", 207 | chksum ? chksum : "null", 208 | chksum ? "\"" : ""); 209 | } 210 | else 211 | { 212 | if (message) 213 | printf("\t\"message\": \"%s\",\n", message); 214 | 215 | printf("\t\"failure\": [ %u, \"%s\" ]\n", errno, strerror(errno)); 216 | 217 | if (st.filefd > -1 && st.filename) 218 | unlink(st.filename); 219 | } 220 | 221 | printf("}\n"); 222 | 223 | return -1; 224 | } 225 | 226 | static int 227 | failure(int code, int e, const char *message) 228 | { 229 | printf("Status: %d %s\r\n", code, message); 230 | printf("Content-Type: text/plain\r\n\r\n"); 231 | printf("%s", message); 232 | 233 | if (e) 234 | printf(": %s", strerror(e)); 235 | 236 | printf("\n"); 237 | 238 | return -1; 239 | } 240 | 241 | static int 242 | filecopy(void) 243 | { 244 | int len; 245 | char buf[READ_BLOCK]; 246 | 247 | if (!st.filedata) 248 | { 249 | close(st.tempfd); 250 | errno = EINVAL; 251 | return response(false, "No file data received"); 252 | } 253 | 254 | snprintf(buf, sizeof(buf), "/proc/self/fd/%d", st.tempfd); 255 | 256 | if (unlink(st.filename) < 0 && errno != ENOENT) 257 | { 258 | close(st.tempfd); 259 | return response(false, "Failed to unlink existing file"); 260 | } 261 | 262 | if (linkat(AT_FDCWD, buf, AT_FDCWD, st.filename, AT_SYMLINK_FOLLOW) < 0) 263 | { 264 | if (lseek(st.tempfd, 0, SEEK_SET) < 0) 265 | { 266 | close(st.tempfd); 267 | return response(false, "Failed to rewind temp file"); 268 | } 269 | 270 | st.filefd = open(st.filename, O_CREAT | O_TRUNC | O_WRONLY, 0600); 271 | 272 | if (st.filefd < 0) 273 | { 274 | close(st.tempfd); 275 | return response(false, "Failed to open target file"); 276 | } 277 | 278 | while ((len = read(st.tempfd, buf, sizeof(buf))) > 0) 279 | { 280 | if (write(st.filefd, buf, len) != len) 281 | { 282 | close(st.tempfd); 283 | close(st.filefd); 284 | return response(false, "I/O failure while writing target file"); 285 | } 286 | } 287 | 288 | close(st.filefd); 289 | } 290 | 291 | close(st.tempfd); 292 | 293 | if (chmod(st.filename, st.filemode)) 294 | return response(false, "Failed to chmod target file"); 295 | 296 | return 0; 297 | } 298 | 299 | static int 300 | header_field(multipart_parser *p, const char *data, size_t len) 301 | { 302 | st.is_content_disposition = !strncasecmp(data, "Content-Disposition", len); 303 | return 0; 304 | } 305 | 306 | static int 307 | header_value(multipart_parser *p, const char *data, size_t len) 308 | { 309 | size_t i, j; 310 | 311 | if (!st.is_content_disposition) 312 | return 0; 313 | 314 | if (len < 10 || strncasecmp(data, "form-data", 9)) 315 | return 0; 316 | 317 | for (data += 9, len -= 9; len > 0 && (*data == ' ' || *data == ';'); data++, len--); 318 | 319 | if (len < 8 || strncasecmp(data, "name=\"", 6)) 320 | return 0; 321 | 322 | for (data += 6, len -= 6, i = 1; i < len; i++) 323 | { 324 | if (data[i] == '"') 325 | { 326 | for (j = 1; j < sizeof(parts) / sizeof(parts[0]); j++) 327 | if (!strncmp(data, parts[j], i - 1)) 328 | st.parttype = j; 329 | 330 | break; 331 | } 332 | } 333 | 334 | return 0; 335 | } 336 | 337 | static int 338 | data_begin_cb(multipart_parser *p) 339 | { 340 | if (st.parttype == PART_FILEDATA) 341 | { 342 | if (!st.sessionid) 343 | return response(false, "File data without session"); 344 | 345 | if (!st.filename) 346 | return response(false, "File data without name"); 347 | 348 | if (!session_access(st.sessionid, "file", st.filename, "write")) 349 | return response(false, "Access to path denied by ACL"); 350 | 351 | st.tempfd = open("/tmp", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR); 352 | 353 | if (st.tempfd < 0) 354 | return response(false, "Failed to create temporary file"); 355 | } 356 | 357 | return 0; 358 | } 359 | 360 | static int 361 | data_cb(multipart_parser *p, const char *data, size_t len) 362 | { 363 | int wlen = len; 364 | 365 | switch (st.parttype) 366 | { 367 | case PART_SESSIONID: 368 | st.sessionid = datadup(data, len); 369 | break; 370 | 371 | case PART_FILENAME: 372 | st.filename = canonicalize_path(data, len); 373 | break; 374 | 375 | case PART_FILEMODE: 376 | st.filemode = strtoul(data, NULL, 8); 377 | break; 378 | 379 | case PART_FILEDATA: 380 | if (write(st.tempfd, data, len) != wlen) 381 | { 382 | close(st.tempfd); 383 | return response(false, "I/O failure while writing temporary file"); 384 | } 385 | 386 | if (!st.filedata) 387 | st.filedata = !!wlen; 388 | 389 | break; 390 | 391 | default: 392 | break; 393 | } 394 | 395 | return 0; 396 | } 397 | 398 | static int 399 | data_end_cb(multipart_parser *p) 400 | { 401 | if (st.parttype == PART_SESSIONID) 402 | { 403 | if (!session_access(st.sessionid, "cgi-io", "upload", "write")) 404 | { 405 | errno = EPERM; 406 | return response(false, "Upload permission denied"); 407 | } 408 | } 409 | else if (st.parttype == PART_FILEDATA) 410 | { 411 | if (st.tempfd < 0) 412 | return response(false, "Internal program failure"); 413 | 414 | #if 0 415 | /* prepare directory */ 416 | for (ptr = st.filename; *ptr; ptr++) 417 | { 418 | if (*ptr == '/') 419 | { 420 | *ptr = 0; 421 | 422 | if (mkdir(st.filename, 0755)) 423 | { 424 | unlink(st.tmpname); 425 | return response(false, "Failed to create destination directory"); 426 | } 427 | 428 | *ptr = '/'; 429 | } 430 | } 431 | #endif 432 | 433 | if (filecopy()) 434 | return -1; 435 | 436 | return response(true, NULL); 437 | } 438 | 439 | st.parttype = PART_UNKNOWN; 440 | return 0; 441 | } 442 | 443 | static multipart_parser * 444 | init_parser(void) 445 | { 446 | char *boundary; 447 | const char *var; 448 | 449 | multipart_parser *p; 450 | static multipart_parser_settings s = { 451 | .on_part_data = data_cb, 452 | .on_headers_complete = data_begin_cb, 453 | .on_part_data_end = data_end_cb, 454 | .on_header_field = header_field, 455 | .on_header_value = header_value 456 | }; 457 | 458 | var = getenv("CONTENT_TYPE"); 459 | 460 | if (!var || strncmp(var, "multipart/form-data;", 20)) 461 | return NULL; 462 | 463 | for (var += 20; *var && *var != '='; var++); 464 | 465 | if (*var++ != '=') 466 | return NULL; 467 | 468 | boundary = malloc(strlen(var) + 3); 469 | 470 | if (!boundary) 471 | return NULL; 472 | 473 | strcpy(boundary, "--"); 474 | strcpy(boundary + 2, var); 475 | 476 | st.tempfd = -1; 477 | st.filefd = -1; 478 | st.filemode = 0600; 479 | 480 | p = multipart_parser_init(boundary, &s); 481 | 482 | free(boundary); 483 | 484 | return p; 485 | } 486 | 487 | static int 488 | main_upload(int argc, char *argv[]) 489 | { 490 | int rem, len; 491 | bool done = false; 492 | char buf[READ_BLOCK]; 493 | multipart_parser *p; 494 | 495 | p = init_parser(); 496 | 497 | if (!p) 498 | { 499 | errno = EINVAL; 500 | return response(false, "Invalid request"); 501 | } 502 | 503 | while ((len = read(0, buf, sizeof(buf))) > 0) 504 | { 505 | if (!done) { 506 | rem = multipart_parser_execute(p, buf, len); 507 | done = (rem < len); 508 | } 509 | } 510 | 511 | multipart_parser_free(p); 512 | 513 | return 0; 514 | } 515 | 516 | static void 517 | free_charp(char **ptr) 518 | { 519 | free(*ptr); 520 | } 521 | 522 | #define autochar __attribute__((__cleanup__(free_charp))) char 523 | 524 | static int 525 | main_download(int argc, char **argv) 526 | { 527 | char *fields[] = { "sessionid", NULL, "path", NULL, "filename", NULL, "mimetype", NULL }; 528 | unsigned long long size = 0; 529 | char *p, buf[READ_BLOCK]; 530 | ssize_t len = 0; 531 | struct stat s; 532 | int rfd; 533 | 534 | autochar *post = postdecode(fields, 4); 535 | (void) post; 536 | 537 | if (!fields[1] || !session_access(fields[1], "cgi-io", "download", "read")) 538 | return failure(403, 0, "Download permission denied"); 539 | 540 | if (!fields[3] || !session_access(fields[1], "file", fields[3], "read")) 541 | return failure(403, 0, "Access to path denied by ACL"); 542 | 543 | if (stat(fields[3], &s)) 544 | return failure(404, errno, "Failed to stat requested path"); 545 | 546 | if (!S_ISREG(s.st_mode) && !S_ISBLK(s.st_mode)) 547 | return failure(403, 0, "Requested path is not a regular file or block device"); 548 | 549 | for (p = fields[5]; p && *p; p++) 550 | if (!isalnum(*p) && !strchr(" ()<>@,;:[]?.=%-", *p)) 551 | return failure(400, 0, "Invalid characters in filename"); 552 | 553 | for (p = fields[7]; p && *p; p++) 554 | if (!isalnum(*p) && !strchr(" .;=/-", *p)) 555 | return failure(400, 0, "Invalid characters in mimetype"); 556 | 557 | rfd = open(fields[3], O_RDONLY); 558 | 559 | if (rfd < 0) 560 | return failure(500, errno, "Failed to open requested path"); 561 | 562 | if (S_ISBLK(s.st_mode)) 563 | ioctl(rfd, BLKGETSIZE64, &size); 564 | else 565 | size = (unsigned long long)s.st_size; 566 | 567 | printf("Status: 200 OK\r\n"); 568 | printf("Content-Type: %s\r\n", fields[7] ? fields[7] : "application/octet-stream"); 569 | 570 | if (fields[5]) 571 | printf("Content-Disposition: attachment; filename=\"%s\"\r\n", fields[5]); 572 | 573 | if (size > 0) { 574 | printf("Content-Length: %llu\r\n\r\n", size); 575 | fflush(stdout); 576 | 577 | while (size > 0) { 578 | len = sendfile(1, rfd, NULL, size); 579 | 580 | if (len == -1) { 581 | if (errno == ENOSYS || errno == EINVAL) { 582 | while ((len = read(rfd, buf, sizeof(buf))) > 0) 583 | fwrite(buf, len, 1, stdout); 584 | 585 | fflush(stdout); 586 | break; 587 | } 588 | 589 | if (errno == EINTR || errno == EAGAIN) 590 | continue; 591 | } 592 | 593 | if (len <= 0) 594 | break; 595 | 596 | size -= len; 597 | } 598 | } 599 | else { 600 | printf("\r\n"); 601 | 602 | while ((len = read(rfd, buf, sizeof(buf))) > 0) 603 | fwrite(buf, len, 1, stdout); 604 | 605 | fflush(stdout); 606 | } 607 | 608 | close(rfd); 609 | 610 | return 0; 611 | } 612 | 613 | static int 614 | main_backup(int argc, char **argv) 615 | { 616 | pid_t pid; 617 | time_t now; 618 | int r; 619 | int len; 620 | int status; 621 | int fds[2]; 622 | char datestr[16] = { 0 }; 623 | char hostname[64] = { 0 }; 624 | char *fields[] = { "sessionid", NULL }; 625 | 626 | autochar *post = postdecode(fields, 1); 627 | (void) post; 628 | 629 | if (!fields[1] || !session_access(fields[1], "cgi-io", "backup", "read")) 630 | return failure(403, 0, "Backup permission denied"); 631 | 632 | if (pipe(fds)) 633 | return failure(500, errno, "Failed to spawn pipe"); 634 | 635 | switch ((pid = fork())) 636 | { 637 | case -1: 638 | return failure(500, errno, "Failed to fork process"); 639 | 640 | case 0: 641 | dup2(fds[1], 1); 642 | 643 | close(0); 644 | close(2); 645 | close(fds[0]); 646 | close(fds[1]); 647 | 648 | r = chdir("/"); 649 | if (r < 0) 650 | return failure(500, errno, "Failed chdir('/')"); 651 | 652 | execl("/sbin/sysupgrade", "/sbin/sysupgrade", 653 | "--create-backup", "-", NULL); 654 | 655 | return -1; 656 | 657 | default: 658 | close(fds[1]); 659 | 660 | now = time(NULL); 661 | strftime(datestr, sizeof(datestr) - 1, "%Y-%m-%d", localtime(&now)); 662 | 663 | if (gethostname(hostname, sizeof(hostname) - 1)) 664 | sprintf(hostname, "OpenWrt"); 665 | 666 | printf("Status: 200 OK\r\n"); 667 | printf("Content-Type: application/x-targz\r\n"); 668 | printf("Content-Disposition: attachment; " 669 | "filename=\"backup-%s-%s.tar.gz\"\r\n\r\n", hostname, datestr); 670 | 671 | fflush(stdout); 672 | 673 | do { 674 | len = splice(fds[0], NULL, 1, NULL, READ_BLOCK, SPLICE_F_MORE); 675 | } while (len > 0 || (len == -1 && errno == EINTR)); 676 | 677 | waitpid(pid, &status, 0); 678 | 679 | close(fds[0]); 680 | 681 | return 0; 682 | } 683 | } 684 | 685 | 686 | static const char * 687 | lookup_executable(const char *cmd) 688 | { 689 | size_t plen = 0, clen; 690 | static char path[PATH_MAX]; 691 | char *search, *p; 692 | struct stat s; 693 | 694 | if (!cmd) 695 | return NULL; 696 | 697 | clen = strlen(cmd) + 1; 698 | 699 | if (!stat(cmd, &s) && S_ISREG(s.st_mode)) 700 | return cmd; 701 | 702 | search = getenv("PATH"); 703 | 704 | if (!search) 705 | search = "/bin:/usr/bin:/sbin:/usr/sbin"; 706 | 707 | p = search; 708 | 709 | do { 710 | if (*p != ':' && *p != '\0') 711 | continue; 712 | 713 | plen = p - search; 714 | 715 | if ((plen + clen) >= sizeof(path)) 716 | continue; 717 | 718 | strncpy(path, search, plen); 719 | sprintf(path + plen, "/%s", cmd); 720 | 721 | if (!stat(path, &s) && S_ISREG(s.st_mode)) 722 | return path; 723 | 724 | search = p + 1; 725 | } while (*p++); 726 | 727 | return NULL; 728 | } 729 | 730 | static int 731 | main_exec(int argc, char **argv) 732 | { 733 | char *fields[] = { "sessionid", NULL, "command", NULL, "filename", NULL, "mimetype", NULL, "stderr", NULL }; 734 | int i, devnull, status, fds[2]; 735 | bool allowed = false, redir_stderr = false; 736 | ssize_t len = 0; 737 | const char *exe; 738 | char *p, **args; 739 | pid_t pid; 740 | 741 | autochar *post = postdecode(fields, 5); 742 | (void) post; 743 | 744 | if (!fields[1] || !session_access(fields[1], "cgi-io", "exec", "read")) 745 | return failure(403, 0, "Exec permission denied"); 746 | 747 | for (p = fields[5]; p && *p; p++) 748 | if (!isalnum(*p) && !strchr(" ()<>@,;:[]?.=%-", *p)) 749 | return failure(400, 0, "Invalid characters in filename"); 750 | 751 | for (p = fields[7]; p && *p; p++) 752 | if (!isalnum(*p) && !strchr(" .;=/-", *p)) 753 | return failure(400, 0, "Invalid characters in mimetype"); 754 | 755 | p = fields[9]; 756 | if (p && p[0] == '1' && p[1] == '\0') 757 | redir_stderr = true; 758 | 759 | args = fields[3] ? parse_command(fields[3]) : NULL; 760 | 761 | if (!args) 762 | return failure(400, 0, "Invalid command parameter"); 763 | 764 | /* First check if we find an ACL match for the whole cmdline ... */ 765 | allowed = session_access(fields[1], "file", args[0], "exec"); 766 | 767 | /* Now split the command vector... */ 768 | for (i = 1; args[i]; i++) 769 | args[i][-1] = 0; 770 | 771 | /* Find executable... */ 772 | exe = lookup_executable(args[0]); 773 | 774 | if (!exe) { 775 | free(args); 776 | return failure(404, 0, "Executable not found"); 777 | } 778 | 779 | /* If there was no ACL match, check for a match on the executable */ 780 | if (!allowed && !session_access(fields[1], "file", exe, "exec")) { 781 | free(args); 782 | return failure(403, 0, "Access to command denied by ACL"); 783 | } 784 | 785 | if (pipe(fds)) { 786 | free(args); 787 | return failure(500, errno, "Failed to spawn pipe"); 788 | } 789 | 790 | switch ((pid = fork())) 791 | { 792 | case -1: 793 | free(args); 794 | close(fds[0]); 795 | close(fds[1]); 796 | return failure(500, errno, "Failed to fork process"); 797 | 798 | case 0: 799 | devnull = open("/dev/null", O_RDWR); 800 | 801 | if (devnull > -1) { 802 | dup2(devnull, 0); 803 | if (!redir_stderr) 804 | dup2(devnull, 2); 805 | close(devnull); 806 | } 807 | else { 808 | close(0); 809 | close(2); 810 | } 811 | 812 | dup2(fds[1], 1); 813 | if (redir_stderr) 814 | dup2(fds[1], 2); 815 | close(fds[0]); 816 | close(fds[1]); 817 | 818 | if (chdir("/") < 0) { 819 | free(args); 820 | return failure(500, errno, "Failed chdir('/')"); 821 | } 822 | 823 | if (execv(exe, args) < 0) { 824 | free(args); 825 | return failure(500, errno, "Failed execv(...)"); 826 | } 827 | 828 | return -1; 829 | 830 | default: 831 | close(fds[1]); 832 | 833 | printf("Status: 200 OK\r\n"); 834 | printf("Content-Type: %s\r\n", 835 | fields[7] ? fields[7] : "application/octet-stream"); 836 | 837 | if (fields[5]) 838 | printf("Content-Disposition: attachment; filename=\"%s\"\r\n", 839 | fields[5]); 840 | 841 | printf("\r\n"); 842 | fflush(stdout); 843 | 844 | do { 845 | len = splice(fds[0], NULL, 1, NULL, READ_BLOCK, SPLICE_F_MORE); 846 | } while (len > 0 || (len == -1 && errno == EINTR)); 847 | 848 | waitpid(pid, &status, 0); 849 | 850 | close(fds[0]); 851 | free(args); 852 | 853 | return 0; 854 | } 855 | } 856 | 857 | int main(int argc, char **argv) 858 | { 859 | if (strstr(argv[0], "cgi-upload")) 860 | return main_upload(argc, argv); 861 | else if (strstr(argv[0], "cgi-download")) 862 | return main_download(argc, argv); 863 | else if (strstr(argv[0], "cgi-backup")) 864 | return main_backup(argc, argv); 865 | else if (strstr(argv[0], "cgi-exec")) 866 | return main_exec(argc, argv); 867 | 868 | return -1; 869 | } 870 | --------------------------------------------------------------------------------