diff options
| author | frosty <gabriel@bwaaa.monster> | 2026-08-16 17:55:19 -0400 |
|---|---|---|
| committer | frosty <gabriel@bwaaa.monster> | 2026-08-16 17:55:19 -0400 |
| commit | acdd9e754a536d542099951e4c9a6acc8cdf1c51 (patch) | |
| tree | 0a1d4333d2b160a8243fe2c35425268cc2690ac9 /src/routing.c | |
| parent | 916d3497b9316ec6b64646d456d3c03141b1fe21 (diff) | |
| download | beaker-acdd9e754a536d542099951e4c9a6acc8cdf1c51.tar.gz | |
Diffstat (limited to 'src/routing.c')
| -rw-r--r-- | src/routing.c | 357 |
1 files changed, 211 insertions, 146 deletions
diff --git a/src/routing.c b/src/routing.c index d5f8285..c0348aa 100644 --- a/src/routing.c +++ b/src/routing.c @@ -61,197 +61,233 @@ const char *get_mime_type(const char *file_path) { return "application/octet-stream"; } +static int hex_value(unsigned char c) { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + return -1; +} + +static int url_decode(char *dst, size_t dst_size, const char *src, + size_t src_len, bool plus_as_space) { + size_t src_index = 0; + size_t dst_index = 0; + + if (dst == NULL || dst_size == 0 || src == NULL) + return -1; + + while (src_index < src_len) { + unsigned char decoded; + if (src[src_index] == '%') { + if (src_index + 2 >= src_len) { + beaker_log("SECURITY", "url_decode: Incomplete percent encoding."); + return -1; + } + int high = hex_value((unsigned char)src[src_index + 1]); + int low = hex_value((unsigned char)src[src_index + 2]); + if (high < 0 || low < 0) { + beaker_log("SECURITY", "url_decode: Invalid percent encoding."); + return -1; + } + decoded = (unsigned char)((high << 4) | low); + src_index += 3; + } else { + decoded = (unsigned char)src[src_index++]; + if (plus_as_space && decoded == '+') + decoded = ' '; + } + + if (decoded == 0 || decoded < 0x20 || decoded == 0x7f) { + beaker_log("SECURITY", "url_decode: Rejected control character."); + return -1; + } + if (dst_index + 1 >= dst_size) { + beaker_log("SECURITY", "url_decode: Decoded value is too long."); + return -1; + } + dst[dst_index++] = (char)decoded; + } + + dst[dst_index] = '\0'; + return 0; +} + static int canonicalize_path(char *canonical, const char *path, size_t max_len) { - char components[MAX_URL_PARAMS][MAX_KEY_LEN]; - int component_count = 0; - - char *path_copy = strdup(path); - if (!path_copy) { + if (canonical == NULL || path == NULL || max_len < 2 || path[0] != '/') { return -1; } - char *token = strtok(path_copy, "/"); - while (token) { - if (strcmp(token, ".") == 0) { + size_t output_len = 1; + canonical[0] = '/'; + canonical[1] = '\0'; - } else if (strcmp(token, "..") == 0) { + const char *cursor = path + 1; + while (*cursor != '\0') { + while (*cursor == '/') + cursor++; + if (*cursor == '\0') + break; - if (component_count > 0) { - component_count--; - } else { + const char *component = cursor; + while (*cursor != '\0' && *cursor != '/') + cursor++; + size_t component_len = (size_t)(cursor - component); - beaker_log("SECURITY", "Path traversal attempt: %s\n", path); - free(path_copy); + if (component_len == 1 && component[0] == '.') { + continue; + } + if (component_len == 2 && component[0] == '.' && component[1] == '.') { + if (output_len == 1) { + beaker_log("SECURITY", "canonicalize_path: Path escapes root."); return -1; } - } else if (strlen(token) > 0) { + while (output_len > 1 && canonical[output_len - 1] != '/') + output_len--; + if (output_len > 1) + output_len--; + canonical[output_len] = '\0'; + continue; + } - if (component_count < MAX_URL_PARAMS) { - strncpy(components[component_count], token, MAX_KEY_LEN - 1); - components[component_count][MAX_KEY_LEN - 1] = '\0'; - component_count++; + for (size_t i = 0; i < component_len; i++) { + unsigned char c = (unsigned char)component[i]; + if (c == '\\' || c < 0x20 || c == 0x7f) { + beaker_log("SECURITY", + "canonicalize_path: Rejected unsafe path character."); + return -1; } } - token = strtok(NULL, "/"); - } - - free(path_copy); - canonical[0] = '\0'; - for (int i = 0; i < component_count; i++) { - if (strlen(canonical) + strlen(components[i]) + 2 > max_len) { - beaker_log("ERROR", "Canonical path too long\n"); + size_t separator_len = output_len > 1 ? 1 : 0; + if (output_len + separator_len + component_len >= max_len) { + beaker_log("SECURITY", "canonicalize_path: Path is too long."); return -1; } - strcat(canonical, "/"); - strcat(canonical, components[i]); - } - - if (canonical[0] == '\0') { - strcpy(canonical, "/"); + if (separator_len != 0) + canonical[output_len++] = '/'; + memcpy(canonical + output_len, component, component_len); + output_len += component_len; + canonical[output_len] = '\0'; } return 0; } -static bool is_safe_path_component(const char *component) { +static int extract_request_target(const char *request_line, char *target, + size_t target_size) { + if (request_line == NULL || target == NULL || target_size == 0) + return -1; - if (strstr(component, "..") || strstr(component, "//")) { - return false; + const char *first_space = strchr(request_line, ' '); + if (first_space == NULL || first_space == request_line || + first_space[1] == ' ') + return -1; + const char *second_space = strchr(first_space + 1, ' '); + if (second_space == NULL || second_space == first_space + 1 || + second_space[1] == '\0' || strchr(second_space + 1, ' ') != NULL || + strchr(request_line, '\t') != NULL) { + return -1; } - for (size_t i = 0; component[i]; i++) { - if (component[i] < 32 && component[i] != '\t') { - return false; - } + size_t method_len = (size_t)(first_space - request_line); + if (method_len >= 16) + return -1; + char method[16]; + memcpy(method, request_line, method_len); + method[method_len] = '\0'; + if (!beaker_is_valid_http_token(method)) + return -1; + + size_t version_len = strlen(second_space + 1); + if (version_len == 0 || version_len >= 16 || + !beaker_is_valid_header_value(second_space + 1)) { + return -1; } - return true; + size_t target_len = (size_t)(second_space - (first_space + 1)); + if (target_len >= target_size) + return -1; + memcpy(target, first_space + 1, target_len); + target[target_len] = '\0'; + return 0; } -static int url_decode(char *dst, const char *src, size_t dst_size) { - size_t i = 0, j = 0; - - while (src[i] && j < dst_size - 1) { - if (src[i] == '%') { +static int parse_query_params(const char *query, UrlParams *params) { + const char *cursor = query; + while (*cursor != '\0') { + const char *pair_end = strchr(cursor, '&'); + if (pair_end == NULL) + pair_end = cursor + strlen(cursor); - if (src[i + 1] && src[i + 2]) { - char hex[3] = {src[i + 1], src[i + 2], '\0'}; - char *endptr; - long value = strtol(hex, &endptr, 16); - - if (*endptr != '\0') { - beaker_log("SECURITY", "Invalid URL encoding: %%%s\n", hex); - return -1; - } - - if (value == 0) { - beaker_log("SECURITY", "Null byte in URL encoding\n"); - return -1; - } - - dst[j++] = (char)value; - i += 3; - } else { - beaker_log("SECURITY", "Incomplete URL encoding at end\n"); + const char *equals = memchr(cursor, '=', (size_t)(pair_end - cursor)); + if (equals != NULL) { + if (params->count >= MAX_URL_PARAMS) { + beaker_log("SECURITY", "parse_query_params: Too many parameters."); return -1; } - } else if (src[i] == '+') { - dst[j++] = ' '; - i++; - } else { - dst[j++] = src[i++]; + UrlParam *param = ¶ms->params[params->count]; + if (url_decode(param->key, sizeof(param->key), cursor, + (size_t)(equals - cursor), true) != 0 || + url_decode(param->value, sizeof(param->value), equals + 1, + (size_t)(pair_end - (equals + 1)), true) != 0) { + return -1; + } + params->count++; } - } - dst[j] = '\0'; + if (*pair_end == '\0') + break; + cursor = pair_end + 1; + } return 0; } char *parse_request_url(const char *request_line, UrlParams *params) { - char method[16]; - char raw_url_full[MAX_PATH_LEN]; - char http_version[16]; - - if (sscanf(request_line, "%15s %255s %15s", method, raw_url_full, - http_version) != 3) { - beaker_log("ERROR", "parse_request_url: Malformed request line\n"); + if (params == NULL) return NULL; - } - params->count = 0; - char *working_raw = strdup(raw_url_full); - if (!working_raw) { - beaker_log_errno("Failed to allocate memory for URL copy"); + char raw_target[MAX_PATH_LEN]; + if (extract_request_target(request_line, raw_target, sizeof(raw_target)) != + 0) { + beaker_log("ERROR", "parse_request_url: Malformed request line."); return NULL; } - char *query_start = strchr(working_raw, '?'); - if (query_start) { - *query_start = '\0'; + char *query_start = strchr(raw_target, '?'); + size_t raw_path_len = query_start == NULL + ? strlen(raw_target) + : (size_t)(query_start - raw_target); + if (raw_path_len == 0 || raw_target[0] != '/') { + beaker_log("SECURITY", "parse_request_url: Invalid request target."); + return NULL; } char decoded_path[MAX_PATH_LEN]; - if (url_decode(decoded_path, working_raw, sizeof(decoded_path)) != 0) { - beaker_log("SECURITY", "Invalid URL encoding in path\n"); - free(working_raw); + if (url_decode(decoded_path, sizeof(decoded_path), raw_target, raw_path_len, + false) != 0) { return NULL; } char canonical_path[MAX_PATH_LEN]; if (canonicalize_path(canonical_path, decoded_path, sizeof(canonical_path)) != 0) { - beaker_log("SECURITY", "Path canonicalization failed\n"); - free(working_raw); return NULL; } - char *path_check = strdup(canonical_path); - if (path_check) { - char *token = strtok(path_check, "/"); - while (token) { - if (!is_safe_path_component(token)) { - beaker_log("SECURITY", "Unsafe path component: %s\n", token); - free(path_check); - free(working_raw); - return NULL; - } - token = strtok(NULL, "/"); - } - free(path_check); - } - - if (query_start) { - char *query_string = query_start + 1; - - char *pair; - char *saveptr; - - pair = strtok_r(query_string, "&", &saveptr); - while (pair && params->count < MAX_URL_PARAMS) { - char *equals = strchr(pair, '='); - if (equals) { - *equals = '\0'; - - char *raw_key = pair; - char *raw_val = equals + 1; - - if (url_decode(params->params[params->count].key, raw_key, - MAX_KEY_LEN) == 0 && - url_decode(params->params[params->count].value, raw_val, - MAX_VALUE_LEN) == 0) { - params->count++; - } - } - pair = strtok_r(NULL, "&", &saveptr); - } + if (query_start != NULL && parse_query_params(query_start + 1, params) != 0) { + params->count = 0; + return NULL; } char *final_path = strdup(canonical_path); - free(working_raw); + if (final_path == NULL) + beaker_log_errno("parse_request_url: Failed to allocate result path"); return final_path; } @@ -305,15 +341,31 @@ bool serve_static_file_with_mime(const char *request_path_relative_to_static, mime_type = get_mime_type(full_static_path); } + if (!beaker_is_valid_header_value(mime_type) || + strlen(mime_type) >= MAX_VALUE_LEN) { + beaker_log("SECURITY", + "serve_static_file_with_mime: Rejected invalid MIME type."); + fclose(fp); + send_status("500 Internal Server Error"); + return true; + } + char http_header[BUFFER_SIZE]; - snprintf(http_header, sizeof(http_header), - "HTTP/1.1 200 OK\r\n" - "Content-Type: %s\r\n" - "Content-Length: %ld\r\n" - "Connection: close\r\n" - "\r\n", - mime_type, file_size); + int header_length = snprintf(http_header, sizeof(http_header), + "HTTP/1.1 200 OK\r\n" + "Content-Type: %s\r\n" + "Content-Length: %ld\r\n" + "Connection: close\r\n" + "\r\n", + mime_type, file_size); + if (header_length < 0 || (size_t)header_length >= sizeof(http_header)) { + beaker_log("ERROR", + "serve_static_file_with_mime: Failed to construct header."); + fclose(fp); + send_status("500 Internal Server Error"); + return true; + } if (beaker_send_all(current_client_socket, http_header, strlen(http_header)) < 0) { @@ -369,17 +421,30 @@ bool serve_data(const char *data, size_t size, const char *mime_type) { return false; } + if (mime_type == NULL || mime_type[0] == '\0' || + !beaker_is_valid_header_value(mime_type) || + strlen(mime_type) >= MAX_VALUE_LEN) { + beaker_log("SECURITY", "serve_data: Rejected invalid MIME type."); + send_status("500 Internal Server Error"); + return false; + } + char http_header[BUFFER_SIZE]; current_response_status = 200; current_response_size = size; - snprintf(http_header, sizeof(http_header), - "HTTP/1.1 200 OK\r\n" - "Content-Type: %s\r\n" - "Content-Length: %zu\r\n" - "Connection: close\r\n" - "\r\n", - mime_type, size); + int header_length = snprintf(http_header, sizeof(http_header), + "HTTP/1.1 200 OK\r\n" + "Content-Type: %s\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n", + mime_type, size); + if (header_length < 0 || (size_t)header_length >= sizeof(http_header)) { + beaker_log("ERROR", "serve_data: Failed to construct header."); + send_status("500 Internal Server Error"); + return false; + } if (beaker_send_all(current_client_socket, http_header, strlen(http_header)) < 0) { |
