Harden HTTP path headers sessions and archives
This commit is contained in:
+98
-12
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user