aboutsummaryrefslogtreecommitdiff
path: root/src/routing.c
diff options
context:
space:
mode:
authorfrosty <gabriel@bwaaa.monster>2026-08-16 17:55:19 -0400
committerfrosty <gabriel@bwaaa.monster>2026-08-16 17:55:19 -0400
commitacdd9e754a536d542099951e4c9a6acc8cdf1c51 (patch)
tree0a1d4333d2b160a8243fe2c35425268cc2690ac9 /src/routing.c
parent916d3497b9316ec6b64646d456d3c03141b1fe21 (diff)
downloadbeaker-acdd9e754a536d542099951e4c9a6acc8cdf1c51.tar.gz
fix: harden HTTP request and response parsingHEADmasterindev
Diffstat (limited to 'src/routing.c')
-rw-r--r--src/routing.c357
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 = &params->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) {