Harden HTTP path headers sessions and archives

This commit is contained in:
udo
2026-05-21 09:56:25 +00:00
parent d37517041d
commit 0d8b74930c
10 changed files with 260 additions and 22 deletions
+98 -12
View File
@@ -36,6 +36,7 @@
#include <cstring>
#include <stdexcept>
#include <cctype>
#include <errno.h> // E*
#include <fcntl.h>
@@ -123,6 +124,86 @@ make_http_text_response(String status_line, String body, String extra_headers =
);
}
static bool
header_token_safe(String name)
{
if(name == "")
return(false);
for(char c : name)
{
if(!(std::isalnum((unsigned char)c) || c == '-' || c == '_'))
return(false);
}
return(true);
}
static String
header_value_sanitize(String value)
{
for(char& c : value)
{
if(c == '\r' || c == '\n')
c = ' ';
}
return(value);
}
static String
render_header_map(StringMap headers)
{
String result;
for(auto& item : headers)
{
if(!header_token_safe(item.first))
continue;
result += item.first + ": " + header_value_sanitize(item.second) + "\r\n";
}
return(result);
}
static String
render_set_cookie_headers(StringList headers)
{
String result;
for(String header : headers)
{
if(header.find('\r') != String::npos || header.find('\n') != String::npos)
continue;
if(!str_starts_with(to_lower(header), "set-cookie: "))
continue;
result += header + "\r\n";
}
return(result);
}
static String
safe_status_line(String status_line)
{
if(status_line.find('\r') != String::npos || status_line.find('\n') != String::npos)
return("Status: 500 Internal Server Error");
return(status_line);
}
static String
http_script_root()
{
if(context && context->server)
return(first(
context->server->config["HTTP_DOCUMENT_ROOT"],
context->server->config["COMPILER_SYS_PATH"],
cwd_get()
));
return(cwd_get());
}
static String
strip_leading_slashes(String s)
{
while(s.length() > 0 && s[0] == '/')
s.erase(0, 1);
return(s);
}
static bool
is_valid_close_code(u16 status_code)
{
@@ -784,15 +865,20 @@ FastCGIServer::process_http_request(FastCGIRequest& request, String& data)
if(!parse_http_message(request, data))
return;
if(request.params["HTTP_SCRIPT_FILENAME"] != "")
request.params["SCRIPT_FILENAME"] = request.params["HTTP_SCRIPT_FILENAME"];
else if(request.params["SCRIPT_FILENAME"] == "" && request.params["DOCUMENT_URI"] != "")
if(request.params["SCRIPT_FILENAME"] == "" && request.params["DOCUMENT_URI"] != "")
{
String document_root = first(request.params["DOCUMENT_ROOT"], cwd_get());
if(document_root.length() > 1 && document_root[document_root.length()-1] == '/')
document_root.resize(document_root.length()-1);
request.params["DOCUMENT_ROOT"] = document_root;
request.params["SCRIPT_FILENAME"] = document_root + request.params["DOCUMENT_URI"];
String document_root = http_script_root();
String document_uri = strip_leading_slashes(request.params["DOCUMENT_URI"]);
String candidate = path_join(document_root, document_uri);
String real_root = path_real(document_root);
String real_candidate = path_real(candidate);
if(real_root == "" || real_candidate == "" || !path_is_within(real_candidate, real_root))
{
reject_http_connection(*client_sockets[request.resources.client_socket], "HTTP/1.1 404 Not Found", "script not found\n");
return;
}
request.params["DOCUMENT_ROOT"] = real_root;
request.params["SCRIPT_FILENAME"] = real_candidate;
}
if(to_lower(request.params["HTTP_UPGRADE"]) == "websocket")
@@ -1253,9 +1339,9 @@ void
FastCGIServer::assemble_output_buffer(FastCGIRequest& request, Connection* connection)
{
request.out =
request.response_code+"\r\n"+
var_dump(request.header, "", "\r\n") +
var_dump(request.set_cookies, "", "\r\n") +
safe_status_line(request.response_code)+"\r\n"+
render_header_map(request.header) +
render_set_cookie_headers(request.set_cookies) +
"\r\n";
for(auto obs : request.ob_stack)
@@ -1268,7 +1354,7 @@ FastCGIServer::assemble_output_buffer(FastCGIRequest& request, Connection* conne
request.set_status(500, "Response Too Large");
request.header.clear();
request.header["Content-Type"] = "text/plain; charset=utf-8";
request.out = request.response_code + "\r\n" + var_dump(request.header, "", "\r\n") + "\r\nresponse exceeded configured output limit\n";
request.out = safe_status_line(request.response_code) + "\r\n" + render_header_map(request.header) + "\r\nresponse exceeded configured output limit\n";
}
request.ob_stack.clear();
request.flags.output_closed = true;
+23
View File
@@ -6,6 +6,7 @@
#include <netdb.h>
#include <execinfo.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/file.h>
#include "sys.h"
#include "hash.h"
@@ -182,6 +183,27 @@ String path_join(String base, String child)
return(base + "/" + child);
}
String path_real(String path)
{
char resolved[PATH_MAX];
if(realpath(path.c_str(), resolved))
return(String(resolved));
return("");
}
bool path_is_within(String path, String root)
{
String real_path = path_real(path);
String real_root = path_real(root);
if(real_path == "" || real_root == "")
return(false);
if(real_path == real_root)
return(true);
if(real_root[real_root.length() - 1] != '/')
real_root += "/";
return(str_starts_with(real_path, real_root));
}
bool mkdir(String path)
{
shell_exec(String("mkdir -p ")+" "+shell_escape(path));
@@ -877,6 +899,7 @@ StringMap make_server_settings()
cfg["TRANSPORT_MAX_RESPONSE_BYTES"] = std::to_string(8 * 1024 * 1024);
cfg["TRANSPORT_HTTP_REQUEST_TIMEOUT_SECONDS"] = "15";
cfg["TRANSPORT_CONNECTION_IDLE_TIMEOUT_SECONDS"] = "120";
cfg["HTTP_DOCUMENT_ROOT"] = "";
cfg["CUSTOM_SERVER_MAX_SERVERS"] = "16";
cfg["CUSTOM_SERVER_MIN_PORT"] = "1024";
cfg["CUSTOM_SERVER_MAX_PORT"] = "65535";
+2
View File
@@ -9,6 +9,8 @@ String shell_escape(String raw);
String basename(String fn);
String dirname(String fn);
String path_join(String base, String child);
String path_real(String path);
bool path_is_within(String path, String root);
bool mkdir(String path);
bool file_exists(String path);
int file_open_locked(String file_name, int open_flags, int lock_type = LOCK_SH, int create_mode = 0644, f64 wait_timeout_seconds = 3.0, String purpose = "");
+63 -4
View File
@@ -1,5 +1,8 @@
#include "uri.h"
#include <fcntl.h>
#include <unistd.h>
static String base64_encode(String raw)
{
static const char* chars =
@@ -256,9 +259,33 @@ String encode_query(StringMap map)
return(result);
}
namespace {
String http_header_value_clean(String value)
{
for(char& c : value)
{
if(c == '\r' || c == '\n')
c = ' ';
}
return(value);
}
String cookie_attribute_value_clean(String value)
{
for(char& c : value)
{
if(c == '\r' || c == '\n' || c == ';')
c = ' ';
}
return(trim(value));
}
}
void redirect(String url, s32 code)
{
context->header["Location"] = url;
context->header["Location"] = http_header_value_clean(url);
context->set_status(code);
}
@@ -499,9 +526,18 @@ void set_cookie(
bool secure, bool http_only)
{
String cookie = "Set-Cookie: ";
cookie.append(uri_encode(name) + "=" + uri_encode(value));
cookie.append(uri_encode(cookie_attribute_value_clean(name)) + "=" + uri_encode(value));
if(expires > 0)
cookie.append(String("; Expires=") + time_format_utc("RFC1123", expires));
if(path != "")
cookie.append("; Path=" + cookie_attribute_value_clean(path));
if(domain != "")
cookie.append("; Domain=" + cookie_attribute_value_clean(domain));
if(secure)
cookie.append("; Secure");
if(http_only)
cookie.append("; HttpOnly");
cookie.append("; SameSite=Lax");
context->set_cookies.push_back(cookie);
context->cookies[name] = value;
}
@@ -520,7 +556,30 @@ StringMap parse_cookies(String cookie_String)
String session_id_create()
{
return(to_hex(rand())+to_hex(rand())+to_hex(rand())+to_hex(rand()));
unsigned char bytes[32];
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
if(fd == -1)
throw std::runtime_error("session_id_create(): could not open /dev/urandom");
size_t offset = 0;
while(offset < sizeof(bytes))
{
ssize_t count = read(fd, bytes + offset, sizeof(bytes) - offset);
if(count <= 0)
{
close(fd);
throw std::runtime_error("session_id_create(): could not read random bytes");
}
offset += (size_t)count;
}
close(fd);
String result;
static const char* hex = "0123456789abcdef";
for(unsigned char b : bytes)
{
result.push_back(hex[b >> 4]);
result.push_back(hex[b & 0x0f]);
}
return(result);
}
bool is_valid_session_id(String session_id)
@@ -602,7 +661,7 @@ String session_start(String session_name)
context->session_name = "";
String session_id = context->cookies[session_name];
if(!is_valid_session_id(session_id))
if(!is_valid_session_id(session_id) || !file_exists(session_file_path(session_id)))
session_id = "";
if(session_id.length() == 0)
+18 -2
View File
@@ -122,6 +122,21 @@ String zip_read(String zip_file_name, String entry_name)
if(!mz_zip_reader_init_file(&archive, zip_file_name.c_str(), 0))
throw std::runtime_error(zip_error("zip_read", "could not open " + zip_file_name));
int file_index = mz_zip_reader_locate_file(&archive, entry_name.c_str(), NULL, 0);
if(file_index < 0)
{
mz_zip_reader_end(&archive);
throw std::runtime_error(zip_error("zip_read", "entry not found or not readable: " + entry_name));
}
mz_zip_archive_file_stat stat;
std::memset(&stat, 0, sizeof(stat));
if(!mz_zip_reader_file_stat(&archive, (mz_uint)file_index, &stat))
{
mz_zip_reader_end(&archive);
throw std::runtime_error(zip_error("zip_read", "entry not found or not readable: " + entry_name));
}
archive_check_size("zip_read", "uncompressed entry", stat.m_uncomp_size, "ARCHIVE_MAX_OUTPUT_BYTES", 64 * 1024 * 1024);
size_t size = 0;
void* data = mz_zip_reader_extract_file_to_heap(&archive, entry_name.c_str(), &size, 0);
if(!data)
@@ -129,7 +144,6 @@ String zip_read(String zip_file_name, String entry_name)
mz_zip_reader_end(&archive);
throw std::runtime_error(zip_error("zip_read", "entry not found or not readable: " + entry_name));
}
archive_check_size("zip_read", "uncompressed entry", size, "ARCHIVE_MAX_OUTPUT_BYTES", 64 * 1024 * 1024);
String result((char*)data, size);
mz_free(data);
mz_zip_reader_end(&archive);
@@ -305,9 +319,12 @@ String gz_compress(String src)
String gz_uncompress(String compressed)
{
archive_check_size("gz_uncompress", "input", compressed.size(), "ARCHIVE_MAX_INPUT_BYTES", 64 * 1024 * 1024);
size_t deflate_offset = gz_deflate_offset(compressed);
size_t footer_offset = compressed.size() - 8;
size_t deflate_size = footer_offset - deflate_offset;
u32 expected_size = gz_read_u32_le(compressed, footer_offset + 4);
archive_check_size("gz_uncompress", "declared output", expected_size, "ARCHIVE_MAX_OUTPUT_BYTES", 64 * 1024 * 1024);
size_t out_len = 0;
void* out = tinfl_decompress_mem_to_heap(compressed.data() + deflate_offset, deflate_size, &out_len, 0);
if(!out)
@@ -318,7 +335,6 @@ String gz_uncompress(String compressed)
mz_free(out);
u32 expected_crc = gz_read_u32_le(compressed, footer_offset);
u32 expected_size = gz_read_u32_le(compressed, footer_offset + 4);
u32 actual_crc = (u32)mz_crc32(MZ_CRC32_INIT, (const unsigned char*)result.data(), result.size());
if(actual_crc != expected_crc)
throw std::runtime_error("gz_uncompress(): crc check failed");
+3 -3
View File
@@ -1329,10 +1329,10 @@ pid_t server_start_http(String key, String socket_fn_or_port, String call_uce_fi
if(call_uce_filename[0] != '/')
call_uce_filename = expand_path(call_uce_filename, cwd_get());
String allowed_root = first(server_state.config["CUSTOM_SERVER_UCE_ROOT"], path_join(server_state.config["COMPILER_SYS_PATH"], server_state.config["SITE_DIRECTORY"]));
if(allowed_root[allowed_root.length() - 1] != '/')
allowed_root += "/";
if(!str_starts_with(call_uce_filename, allowed_root))
String real_call_uce_filename = path_real(call_uce_filename);
if(real_call_uce_filename == "" || !path_is_within(real_call_uce_filename, allowed_root))
throw std::runtime_error("server_start_http(): call_uce_filename is outside configured custom server UCE root");
call_uce_filename = real_call_uce_filename;
String config_file = custom_server_config_file(key);
StringMap previous_config;